rdb_ocean_setup.F90 Source File

Ocean slot configuration from a parsed namelist config.


This file depends on

sourcefile~~rdb_ocean_setup.f90~~EfferentGraph sourcefile~rdb_ocean_setup.f90 rdb_ocean_setup.F90 sourcefile~rdb_config.f90 rdb_config.F90 sourcefile~rdb_ocean_setup.f90->sourcefile~rdb_config.f90 sourcefile~rdb_constants.f90 rdb_constants.F90 sourcefile~rdb_ocean_setup.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_coriolis_adv.f90 rdb_coriolis_adv.F90 sourcefile~rdb_ocean_setup.f90->sourcefile~rdb_coriolis_adv.f90 sourcefile~rdb_decomp.f90 rdb_decomp.F90 sourcefile~rdb_ocean_setup.f90->sourcefile~rdb_decomp.f90 sourcefile~rdb_eos.f90 rdb_eos.F90 sourcefile~rdb_ocean_setup.f90->sourcefile~rdb_eos.f90 sourcefile~rdb_error_ring.f90 rdb_error_ring.F90 sourcefile~rdb_ocean_setup.f90->sourcefile~rdb_error_ring.f90 sourcefile~rdb_grid.f90 rdb_grid.F90 sourcefile~rdb_ocean_setup.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_halo.f90 rdb_halo.F90 sourcefile~rdb_ocean_setup.f90->sourcefile~rdb_halo.f90 sourcefile~rdb_ocean_bottom_drag.f90 rdb_ocean_bottom_drag.F90 sourcefile~rdb_ocean_setup.f90->sourcefile~rdb_ocean_bottom_drag.f90 sourcefile~rdb_ocean_boundary_types.f90 rdb_ocean_boundary_types.F90 sourcefile~rdb_ocean_setup.f90->sourcefile~rdb_ocean_boundary_types.f90 sourcefile~rdb_ocean_cavity.f90 rdb_ocean_cavity.F90 sourcefile~rdb_ocean_setup.f90->sourcefile~rdb_ocean_cavity.f90 sourcefile~rdb_ocean_cavity_melt.f90 rdb_ocean_cavity_melt.F90 sourcefile~rdb_ocean_setup.f90->sourcefile~rdb_ocean_cavity_melt.f90 sourcefile~rdb_ocean_dyn.f90 rdb_ocean_dyn.F90 sourcefile~rdb_ocean_setup.f90->sourcefile~rdb_ocean_dyn.f90 sourcefile~rdb_ocean_epbl.f90 rdb_ocean_epbl.F90 sourcefile~rdb_ocean_setup.f90->sourcefile~rdb_ocean_epbl.f90 sourcefile~rdb_ocean_fold.f90 rdb_ocean_fold.F90 sourcefile~rdb_ocean_setup.f90->sourcefile~rdb_ocean_fold.f90 sourcefile~rdb_ocean_fold_apply.f90 rdb_ocean_fold_apply.F90 sourcefile~rdb_ocean_setup.f90->sourcefile~rdb_ocean_fold_apply.f90 sourcefile~rdb_ocean_fold_exchange.f90 rdb_ocean_fold_exchange.F90 sourcefile~rdb_ocean_setup.f90->sourcefile~rdb_ocean_fold_exchange.f90 sourcefile~rdb_ocean_geothermal.f90 rdb_ocean_geothermal.F90 sourcefile~rdb_ocean_setup.f90->sourcefile~rdb_ocean_geothermal.f90 sourcefile~rdb_ocean_halo.f90 rdb_ocean_halo.F90 sourcefile~rdb_ocean_setup.f90->sourcefile~rdb_ocean_halo.f90 sourcefile~rdb_ocean_halo_state.f90 rdb_ocean_halo_state.F90 sourcefile~rdb_ocean_setup.f90->sourcefile~rdb_ocean_halo_state.f90 sourcefile~rdb_ocean_horizontal_viscosity.f90 rdb_ocean_horizontal_viscosity.F90 sourcefile~rdb_ocean_setup.f90->sourcefile~rdb_ocean_horizontal_viscosity.f90 sourcefile~rdb_ocean_lateral_mix.f90 rdb_ocean_lateral_mix.F90 sourcefile~rdb_ocean_setup.f90->sourcefile~rdb_ocean_lateral_mix.f90 sourcefile~rdb_ocean_metrics.f90 rdb_ocean_metrics.F90 sourcefile~rdb_ocean_setup.f90->sourcefile~rdb_ocean_metrics.f90 sourcefile~rdb_ocean_p_surf.f90 rdb_ocean_p_surf.F90 sourcefile~rdb_ocean_setup.f90->sourcefile~rdb_ocean_p_surf.f90 sourcefile~rdb_ocean_porous.f90 rdb_ocean_porous.F90 sourcefile~rdb_ocean_setup.f90->sourcefile~rdb_ocean_porous.f90 sourcefile~rdb_ocean_pressure_force.f90 rdb_ocean_pressure_force.F90 sourcefile~rdb_ocean_setup.f90->sourcefile~rdb_ocean_pressure_force.f90 sourcefile~rdb_ocean_sponge.f90 rdb_ocean_sponge.F90 sourcefile~rdb_ocean_setup.f90->sourcefile~rdb_ocean_sponge.f90 sourcefile~rdb_ocean_state.f90 rdb_ocean_state.F90 sourcefile~rdb_ocean_setup.f90->sourcefile~rdb_ocean_state.f90 sourcefile~rdb_ocean_status.f90 rdb_ocean_status.F90 sourcefile~rdb_ocean_setup.f90->sourcefile~rdb_ocean_status.f90 sourcefile~rdb_ocean_surface_flux.f90 rdb_ocean_surface_flux.F90 sourcefile~rdb_ocean_setup.f90->sourcefile~rdb_ocean_surface_flux.f90 sourcefile~rdb_ocean_surface_stress.f90 rdb_ocean_surface_stress.F90 sourcefile~rdb_ocean_setup.f90->sourcefile~rdb_ocean_surface_stress.f90 sourcefile~rdb_ocean_tide_astro.f90 rdb_ocean_tide_astro.F90 sourcefile~rdb_ocean_setup.f90->sourcefile~rdb_ocean_tide_astro.f90 sourcefile~rdb_ocean_tides.f90 rdb_ocean_tides.F90 sourcefile~rdb_ocean_setup.f90->sourcefile~rdb_ocean_tides.f90 sourcefile~rdb_ocean_top_drag.f90 rdb_ocean_top_drag.F90 sourcefile~rdb_ocean_setup.f90->sourcefile~rdb_ocean_top_drag.f90 sourcefile~rdb_ocean_vcoord.f90 rdb_ocean_vcoord.F90 sourcefile~rdb_ocean_setup.f90->sourcefile~rdb_ocean_vcoord.f90 sourcefile~rdb_ocean_vdiff.f90 rdb_ocean_vdiff.F90 sourcefile~rdb_ocean_setup.f90->sourcefile~rdb_ocean_vdiff.f90 sourcefile~rdb_ocean_vmix.f90 rdb_ocean_vmix.F90 sourcefile~rdb_ocean_setup.f90->sourcefile~rdb_ocean_vmix.f90 sourcefile~rdb_recon_weno.f90 rdb_recon_weno.F90 sourcefile~rdb_ocean_setup.f90->sourcefile~rdb_recon_weno.f90 sourcefile~rdb_vcoord.f90 rdb_vcoord.F90 sourcefile~rdb_ocean_setup.f90->sourcefile~rdb_vcoord.f90 sourcefile~rdb_config.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_config.f90->sourcefile~rdb_coriolis_adv.f90 sourcefile~rdb_config.f90->sourcefile~rdb_eos.f90 sourcefile~rdb_config.f90->sourcefile~rdb_error_ring.f90 sourcefile~rdb_config.f90->sourcefile~rdb_ocean_bottom_drag.f90 sourcefile~rdb_config.f90->sourcefile~rdb_ocean_boundary_types.f90 sourcefile~rdb_config.f90->sourcefile~rdb_ocean_cavity.f90 sourcefile~rdb_config.f90->sourcefile~rdb_ocean_cavity_melt.f90 sourcefile~rdb_config.f90->sourcefile~rdb_ocean_horizontal_viscosity.f90 sourcefile~rdb_config.f90->sourcefile~rdb_ocean_lateral_mix.f90 sourcefile~rdb_config.f90->sourcefile~rdb_ocean_pressure_force.f90 sourcefile~rdb_config.f90->sourcefile~rdb_ocean_status.f90 sourcefile~rdb_config.f90->sourcefile~rdb_ocean_surface_flux.f90 sourcefile~rdb_config.f90->sourcefile~rdb_ocean_top_drag.f90 sourcefile~rdb_config.f90->sourcefile~rdb_ocean_vmix.f90 sourcefile~rdb_config.f90->sourcefile~rdb_recon_weno.f90 sourcefile~rdb_config.f90->sourcefile~rdb_vcoord.f90 sourcefile~rdb_ice_enthalpy.f90 rdb_ice_enthalpy.F90 sourcefile~rdb_config.f90->sourcefile~rdb_ice_enthalpy.f90 sourcefile~rdb_ice_init.f90 rdb_ice_init.F90 sourcefile~rdb_config.f90->sourcefile~rdb_ice_init.f90 sourcefile~rdb_nml_schema.f90 rdb_nml_schema.F90 sourcefile~rdb_config.f90->sourcefile~rdb_nml_schema.f90 sourcefile~rdb_ocean_pseudo_salt.f90 rdb_ocean_pseudo_salt.F90 sourcefile~rdb_config.f90->sourcefile~rdb_ocean_pseudo_salt.f90 sourcefile~rdb_ocean_tidal_mixing.f90 rdb_ocean_tidal_mixing.F90 sourcefile~rdb_config.f90->sourcefile~rdb_ocean_tidal_mixing.f90 sourcefile~rdb_coriolis_adv.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_coriolis_adv.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_coriolis_adv.f90->sourcefile~rdb_ocean_metrics.f90 sourcefile~rdb_coriolis_adv.f90->sourcefile~rdb_ocean_porous.f90 sourcefile~rdb_barotropic_state.f90 rdb_barotropic_state.F90 sourcefile~rdb_coriolis_adv.f90->sourcefile~rdb_barotropic_state.f90 sourcefile~rdb_mem_report.f90 rdb_mem_report.F90 sourcefile~rdb_coriolis_adv.f90->sourcefile~rdb_mem_report.f90 sourcefile~rdb_multilayer_state.f90 rdb_multilayer_state.F90 sourcefile~rdb_coriolis_adv.f90->sourcefile~rdb_multilayer_state.f90 sourcefile~rdb_scratch_3d.f90 rdb_scratch_3d.F90 sourcefile~rdb_coriolis_adv.f90->sourcefile~rdb_scratch_3d.f90 sourcefile~rdb_decomp.f90->sourcefile~rdb_config.f90 sourcefile~rdb_eos.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_eos.f90->sourcefile~rdb_error_ring.f90 sourcefile~rdb_eos.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_eos.f90->sourcefile~rdb_ocean_status.f90 sourcefile~rdb_grid.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_halo.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_halo.f90->sourcefile~rdb_decomp.f90 sourcefile~rdb_comm_env.f90 rdb_comm_env.F90 sourcefile~rdb_halo.f90->sourcefile~rdb_comm_env.f90 sourcefile~rdb_efp.f90 rdb_efp.F90 sourcefile~rdb_halo.f90->sourcefile~rdb_efp.f90 sourcefile~rdb_ocean_bottom_drag.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ocean_bottom_drag.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_ocean_bottom_drag.f90->sourcefile~rdb_ocean_metrics.f90 sourcefile~rdb_ocean_bottom_drag.f90->sourcefile~rdb_mem_report.f90 sourcefile~rdb_ocean_bottom_drag.f90->sourcefile~rdb_multilayer_state.f90 sourcefile~rdb_ocean_bottom_drag.f90->sourcefile~rdb_scratch_3d.f90 sourcefile~rdb_ocean_boundary_types.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ocean_boundary_types.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_ocean_boundary_types.f90->sourcefile~rdb_ocean_status.f90 sourcefile~rdb_ocean_boundary_types.f90->sourcefile~rdb_ocean_tide_astro.f90 sourcefile~rdb_ocean_boundary_types.f90->sourcefile~rdb_mem_report.f90 sourcefile~rdb_ocean_cavity.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ocean_cavity.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_ocean_cavity_melt.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ocean_cavity_melt.f90->sourcefile~rdb_eos.f90 sourcefile~rdb_ocean_dyn.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ocean_dyn.f90->sourcefile~rdb_coriolis_adv.f90 sourcefile~rdb_ocean_dyn.f90->sourcefile~rdb_eos.f90 sourcefile~rdb_ocean_dyn.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_ocean_dyn.f90->sourcefile~rdb_ocean_bottom_drag.f90 sourcefile~rdb_ocean_dyn.f90->sourcefile~rdb_ocean_boundary_types.f90 sourcefile~rdb_ocean_dyn.f90->sourcefile~rdb_ocean_epbl.f90 sourcefile~rdb_ocean_dyn.f90->sourcefile~rdb_ocean_fold_apply.f90 sourcefile~rdb_ocean_dyn.f90->sourcefile~rdb_ocean_geothermal.f90 sourcefile~rdb_ocean_dyn.f90->sourcefile~rdb_ocean_halo.f90 sourcefile~rdb_ocean_dyn.f90->sourcefile~rdb_ocean_halo_state.f90 sourcefile~rdb_ocean_dyn.f90->sourcefile~rdb_ocean_horizontal_viscosity.f90 sourcefile~rdb_ocean_dyn.f90->sourcefile~rdb_ocean_lateral_mix.f90 sourcefile~rdb_ocean_dyn.f90->sourcefile~rdb_ocean_metrics.f90 sourcefile~rdb_ocean_dyn.f90->sourcefile~rdb_ocean_p_surf.f90 sourcefile~rdb_ocean_dyn.f90->sourcefile~rdb_ocean_porous.f90 sourcefile~rdb_ocean_dyn.f90->sourcefile~rdb_ocean_pressure_force.f90 sourcefile~rdb_ocean_dyn.f90->sourcefile~rdb_ocean_sponge.f90 sourcefile~rdb_ocean_dyn.f90->sourcefile~rdb_ocean_surface_flux.f90 sourcefile~rdb_ocean_dyn.f90->sourcefile~rdb_ocean_surface_stress.f90 sourcefile~rdb_ocean_dyn.f90->sourcefile~rdb_ocean_tides.f90 sourcefile~rdb_ocean_dyn.f90->sourcefile~rdb_ocean_top_drag.f90 sourcefile~rdb_ocean_dyn.f90->sourcefile~rdb_ocean_vcoord.f90 sourcefile~rdb_ocean_dyn.f90->sourcefile~rdb_ocean_vdiff.f90 sourcefile~rdb_ocean_dyn.f90->sourcefile~rdb_ocean_vmix.f90 sourcefile~rdb_barotropic_coupling.f90 rdb_barotropic_coupling.F90 sourcefile~rdb_ocean_dyn.f90->sourcefile~rdb_barotropic_coupling.f90 sourcefile~rdb_ocean_dyn.f90->sourcefile~rdb_barotropic_state.f90 sourcefile~rdb_barotropic_substep.f90 rdb_barotropic_substep.F90 sourcefile~rdb_ocean_dyn.f90->sourcefile~rdb_barotropic_substep.f90 sourcefile~rdb_barotropic_workstate.f90 rdb_barotropic_workstate.F90 sourcefile~rdb_ocean_dyn.f90->sourcefile~rdb_barotropic_workstate.f90 sourcefile~rdb_continuity.f90 rdb_continuity.F90 sourcefile~rdb_ocean_dyn.f90->sourcefile~rdb_continuity.f90 sourcefile~rdb_ocean_dyn.f90->sourcefile~rdb_efp.f90 sourcefile~rdb_ocean_dyn.f90->sourcefile~rdb_mem_report.f90 sourcefile~rdb_ocean_dyn.f90->sourcefile~rdb_multilayer_state.f90 sourcefile~rdb_ocean_bt_budget_probe.f90 rdb_ocean_bt_budget_probe.F90 sourcefile~rdb_ocean_dyn.f90->sourcefile~rdb_ocean_bt_budget_probe.f90 sourcefile~rdb_ocean_bt_wide.f90 rdb_ocean_bt_wide.F90 sourcefile~rdb_ocean_dyn.f90->sourcefile~rdb_ocean_bt_wide.f90 sourcefile~rdb_ocean_cavity_flux.f90 rdb_ocean_cavity_flux.F90 sourcefile~rdb_ocean_dyn.f90->sourcefile~rdb_ocean_cavity_flux.f90 sourcefile~rdb_ocean_chksum.f90 rdb_ocean_chksum.F90 sourcefile~rdb_ocean_dyn.f90->sourcefile~rdb_ocean_chksum.f90 sourcefile~rdb_ocean_console_stats.f90 rdb_ocean_console_stats.F90 sourcefile~rdb_ocean_dyn.f90->sourcefile~rdb_ocean_console_stats.f90 sourcefile~rdb_ocean_eos_compute.f90 rdb_ocean_eos_compute.F90 sourcefile~rdb_ocean_dyn.f90->sourcefile~rdb_ocean_eos_compute.f90 sourcefile~rdb_ocean_ghost_poison.f90 rdb_ocean_ghost_poison.F90 sourcefile~rdb_ocean_dyn.f90->sourcefile~rdb_ocean_ghost_poison.f90 sourcefile~rdb_ocean_gm.f90 rdb_ocean_gm.F90 sourcefile~rdb_ocean_dyn.f90->sourcefile~rdb_ocean_gm.f90 sourcefile~rdb_ocean_hdiff_tracer.f90 rdb_ocean_hdiff_tracer.F90 sourcefile~rdb_ocean_dyn.f90->sourcefile~rdb_ocean_hdiff_tracer.f90 sourcefile~rdb_ocean_ideal_age.f90 rdb_ocean_ideal_age.F90 sourcefile~rdb_ocean_dyn.f90->sourcefile~rdb_ocean_ideal_age.f90 sourcefile~rdb_ocean_isopycnal_slopes.f90 rdb_ocean_isopycnal_slopes.F90 sourcefile~rdb_ocean_dyn.f90->sourcefile~rdb_ocean_isopycnal_slopes.f90 sourcefile~rdb_ocean_kappa_shear.f90 rdb_ocean_kappa_shear.F90 sourcefile~rdb_ocean_dyn.f90->sourcefile~rdb_ocean_kappa_shear.f90 sourcefile~rdb_ocean_ke_probe.f90 rdb_ocean_ke_probe.F90 sourcefile~rdb_ocean_dyn.f90->sourcefile~rdb_ocean_ke_probe.f90 sourcefile~rdb_ocean_meke.f90 rdb_ocean_meke.F90 sourcefile~rdb_ocean_dyn.f90->sourcefile~rdb_ocean_meke.f90 sourcefile~rdb_ocean_min_thickness.f90 rdb_ocean_min_thickness.F90 sourcefile~rdb_ocean_dyn.f90->sourcefile~rdb_ocean_min_thickness.f90 sourcefile~rdb_ocean_mle.f90 rdb_ocean_mle.F90 sourcefile~rdb_ocean_dyn.f90->sourcefile~rdb_ocean_mle.f90 sourcefile~rdb_ocean_obc_baroclinic.f90 rdb_ocean_obc_baroclinic.F90 sourcefile~rdb_ocean_dyn.f90->sourcefile~rdb_ocean_obc_baroclinic.f90 sourcefile~rdb_ocean_periodic.f90 rdb_ocean_periodic.F90 sourcefile~rdb_ocean_dyn.f90->sourcefile~rdb_ocean_periodic.f90 sourcefile~rdb_ocean_redi.f90 rdb_ocean_redi.F90 sourcefile~rdb_ocean_dyn.f90->sourcefile~rdb_ocean_redi.f90 sourcefile~rdb_ocean_remap.f90 rdb_ocean_remap.F90 sourcefile~rdb_ocean_dyn.f90->sourcefile~rdb_ocean_remap.f90 sourcefile~rdb_ocean_dyn.f90->sourcefile~rdb_ocean_tidal_mixing.f90 sourcefile~rdb_ocean_varmix.f90 rdb_ocean_varmix.F90 sourcefile~rdb_ocean_dyn.f90->sourcefile~rdb_ocean_varmix.f90 sourcefile~rdb_ocean_vertical_advection.f90 rdb_ocean_vertical_advection.F90 sourcefile~rdb_ocean_dyn.f90->sourcefile~rdb_ocean_vertical_advection.f90 sourcefile~rdb_ocean_wave_speed.f90 rdb_ocean_wave_speed.F90 sourcefile~rdb_ocean_dyn.f90->sourcefile~rdb_ocean_wave_speed.f90 sourcefile~rdb_profiler.f90 rdb_profiler.F90 sourcefile~rdb_ocean_dyn.f90->sourcefile~rdb_profiler.f90 sourcefile~rdb_ocean_epbl.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ocean_epbl.f90->sourcefile~rdb_eos.f90 sourcefile~rdb_ocean_epbl.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_ocean_epbl.f90->sourcefile~rdb_ocean_surface_flux.f90 sourcefile~rdb_ocean_epbl.f90->sourcefile~rdb_ocean_surface_stress.f90 sourcefile~rdb_ocean_epbl.f90->sourcefile~rdb_mem_report.f90 sourcefile~rdb_ocean_epbl.f90->sourcefile~rdb_multilayer_state.f90 sourcefile~rdb_ocean_epbl.f90->sourcefile~rdb_scratch_3d.f90 sourcefile~rdb_ocean_fold.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ocean_fold_apply.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ocean_fold_apply.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_ocean_fold_apply.f90->sourcefile~rdb_ocean_boundary_types.f90 sourcefile~rdb_ocean_fold_apply.f90->sourcefile~rdb_ocean_fold.f90 sourcefile~rdb_ocean_fold_apply.f90->sourcefile~rdb_ocean_fold_exchange.f90 sourcefile~rdb_ocean_fold_apply.f90->sourcefile~rdb_multilayer_state.f90 sourcefile~rdb_ocean_fold_exchange.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ocean_fold_exchange.f90->sourcefile~rdb_decomp.f90 sourcefile~rdb_ocean_fold_exchange.f90->sourcefile~rdb_error_ring.f90 sourcefile~rdb_ocean_fold_exchange.f90->sourcefile~rdb_ocean_fold.f90 sourcefile~rdb_ocean_fold_exchange.f90->sourcefile~rdb_ocean_status.f90 sourcefile~rdb_ocean_fold_exchange.f90->sourcefile~rdb_comm_env.f90 sourcefile~rdb_ocean_fold_plan.f90 rdb_ocean_fold_plan.F90 sourcefile~rdb_ocean_fold_exchange.f90->sourcefile~rdb_ocean_fold_plan.f90 sourcefile~rdb_ocean_geothermal.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ocean_geothermal.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_ocean_geothermal.f90->sourcefile~rdb_ocean_surface_flux.f90 sourcefile~rdb_ocean_geothermal.f90->sourcefile~rdb_multilayer_state.f90 sourcefile~rdb_ocean_halo.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ocean_halo.f90->sourcefile~rdb_decomp.f90 sourcefile~rdb_ocean_halo.f90->sourcefile~rdb_error_ring.f90 sourcefile~rdb_ocean_halo.f90->sourcefile~rdb_ocean_status.f90 sourcefile~rdb_ocean_halo.f90->sourcefile~rdb_comm_env.f90 sourcefile~rdb_ocean_halo_counters.f90 rdb_ocean_halo_counters.F90 sourcefile~rdb_ocean_halo.f90->sourcefile~rdb_ocean_halo_counters.f90 sourcefile~rdb_ocean_halo.f90->sourcefile~rdb_ocean_periodic.f90 sourcefile~rdb_ocean_halo_state.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ocean_halo_state.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_ocean_halo_state.f90->sourcefile~rdb_ocean_boundary_types.f90 sourcefile~rdb_ocean_halo_state.f90->sourcefile~rdb_ocean_fold_apply.f90 sourcefile~rdb_ocean_halo_state.f90->sourcefile~rdb_ocean_halo.f90 sourcefile~rdb_ocean_halo_state.f90->sourcefile~rdb_ocean_surface_stress.f90 sourcefile~rdb_ice_state.f90 rdb_ice_state.F90 sourcefile~rdb_ocean_halo_state.f90->sourcefile~rdb_ice_state.f90 sourcefile~rdb_ocean_halo_state.f90->sourcefile~rdb_multilayer_state.f90 sourcefile~rdb_ocean_halo_state.f90->sourcefile~rdb_ocean_halo_counters.f90 sourcefile~rdb_ocean_halo_state.f90->sourcefile~rdb_ocean_periodic.f90 sourcefile~rdb_ocean_halo_state.f90->sourcefile~rdb_profiler.f90 sourcefile~rdb_ocean_horizontal_viscosity.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ocean_horizontal_viscosity.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_ocean_horizontal_viscosity.f90->sourcefile~rdb_ocean_lateral_mix.f90 sourcefile~rdb_ocean_horizontal_viscosity.f90->sourcefile~rdb_ocean_metrics.f90 sourcefile~rdb_ocean_horizontal_viscosity.f90->sourcefile~rdb_mem_report.f90 sourcefile~rdb_ocean_horizontal_viscosity.f90->sourcefile~rdb_multilayer_state.f90 sourcefile~rdb_ocean_horizontal_viscosity.f90->sourcefile~rdb_scratch_3d.f90 sourcefile~rdb_ocean_lateral_mix.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ocean_lateral_mix.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_ocean_lateral_mix.f90->sourcefile~rdb_ocean_metrics.f90 sourcefile~rdb_ocean_lateral_mix.f90->sourcefile~rdb_mem_report.f90 sourcefile~rdb_ocean_lateral_mix.f90->sourcefile~rdb_multilayer_state.f90 sourcefile~rdb_ocean_lateral_mix.f90->sourcefile~rdb_scratch_3d.f90 sourcefile~rdb_ocean_metrics.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ocean_metrics.f90->sourcefile~rdb_error_ring.f90 sourcefile~rdb_ocean_metrics.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_ocean_metrics.f90->sourcefile~rdb_ocean_fold.f90 sourcefile~rdb_ocean_metrics.f90->sourcefile~rdb_ocean_status.f90 sourcefile~rdb_io_netcdf.f90 rdb_io_netcdf.F90 sourcefile~rdb_ocean_metrics.f90->sourcefile~rdb_io_netcdf.f90 sourcefile~rdb_ocean_metrics.f90->sourcefile~rdb_mem_report.f90 sourcefile~rdb_ocean_bipolar.f90 rdb_ocean_bipolar.F90 sourcefile~rdb_ocean_metrics.f90->sourcefile~rdb_ocean_bipolar.f90 sourcefile~rdb_ocean_p_surf.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ocean_p_surf.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_ocean_p_surf.f90->sourcefile~rdb_mem_report.f90 sourcefile~rdb_ocean_porous.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ocean_pressure_force.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ocean_pressure_force.f90->sourcefile~rdb_eos.f90 sourcefile~rdb_ocean_pressure_force.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_ocean_pressure_force.f90->sourcefile~rdb_ocean_metrics.f90 sourcefile~rdb_ocean_pressure_force.f90->sourcefile~rdb_mem_report.f90 sourcefile~rdb_ocean_pressure_force.f90->sourcefile~rdb_multilayer_state.f90 sourcefile~rdb_ocean_pgf_reconstruct.f90 rdb_ocean_pgf_reconstruct.F90 sourcefile~rdb_ocean_pressure_force.f90->sourcefile~rdb_ocean_pgf_reconstruct.f90 sourcefile~rdb_ocean_pressure_force.f90->sourcefile~rdb_scratch_3d.f90 sourcefile~rdb_ocean_sponge.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ocean_sponge.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_ocean_sponge.f90->sourcefile~rdb_ocean_boundary_types.f90 sourcefile~rdb_ocean_sponge.f90->sourcefile~rdb_mem_report.f90 sourcefile~rdb_ocean_sponge.f90->sourcefile~rdb_multilayer_state.f90 sourcefile~rdb_ocean_state.f90->sourcefile~rdb_config.f90 sourcefile~rdb_ocean_state.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ocean_state.f90->sourcefile~rdb_coriolis_adv.f90 sourcefile~rdb_ocean_state.f90->sourcefile~rdb_decomp.f90 sourcefile~rdb_ocean_state.f90->sourcefile~rdb_eos.f90 sourcefile~rdb_ocean_state.f90->sourcefile~rdb_error_ring.f90 sourcefile~rdb_ocean_state.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_ocean_state.f90->sourcefile~rdb_ocean_bottom_drag.f90 sourcefile~rdb_ocean_state.f90->sourcefile~rdb_ocean_boundary_types.f90 sourcefile~rdb_ocean_state.f90->sourcefile~rdb_ocean_cavity.f90 sourcefile~rdb_ocean_state.f90->sourcefile~rdb_ocean_dyn.f90 sourcefile~rdb_ocean_state.f90->sourcefile~rdb_ocean_epbl.f90 sourcefile~rdb_ocean_state.f90->sourcefile~rdb_ocean_fold.f90 sourcefile~rdb_ocean_state.f90->sourcefile~rdb_ocean_horizontal_viscosity.f90 sourcefile~rdb_ocean_state.f90->sourcefile~rdb_ocean_lateral_mix.f90 sourcefile~rdb_ocean_state.f90->sourcefile~rdb_ocean_metrics.f90 sourcefile~rdb_ocean_state.f90->sourcefile~rdb_ocean_p_surf.f90 sourcefile~rdb_ocean_state.f90->sourcefile~rdb_ocean_pressure_force.f90 sourcefile~rdb_ocean_state.f90->sourcefile~rdb_ocean_sponge.f90 sourcefile~rdb_ocean_state.f90->sourcefile~rdb_ocean_status.f90 sourcefile~rdb_ocean_state.f90->sourcefile~rdb_ocean_surface_flux.f90 sourcefile~rdb_ocean_state.f90->sourcefile~rdb_ocean_surface_stress.f90 sourcefile~rdb_ocean_state.f90->sourcefile~rdb_ocean_tides.f90 sourcefile~rdb_ocean_state.f90->sourcefile~rdb_ocean_top_drag.f90 sourcefile~rdb_ocean_state.f90->sourcefile~rdb_ocean_vcoord.f90 sourcefile~rdb_ocean_state.f90->sourcefile~rdb_ocean_vdiff.f90 sourcefile~rdb_ocean_state.f90->sourcefile~rdb_ocean_vmix.f90 sourcefile~rdb_ocean_state.f90->sourcefile~rdb_barotropic_state.f90 sourcefile~rdb_bathymetry.f90 rdb_bathymetry.F90 sourcefile~rdb_ocean_state.f90->sourcefile~rdb_bathymetry.f90 sourcefile~rdb_ocean_state.f90->sourcefile~rdb_comm_env.f90 sourcefile~rdb_ocean_state.f90->sourcefile~rdb_continuity.f90 sourcefile~rdb_ocean_state.f90->sourcefile~rdb_ice_state.f90 sourcefile~rdb_ocean_state.f90->sourcefile~rdb_multilayer_state.f90 sourcefile~rdb_ocean_bathymetry_inject.f90 rdb_ocean_bathymetry_inject.F90 sourcefile~rdb_ocean_state.f90->sourcefile~rdb_ocean_bathymetry_inject.f90 sourcefile~rdb_ocean_state.f90->sourcefile~rdb_ocean_cavity_flux.f90 sourcefile~rdb_ocean_data_forcing.f90 rdb_ocean_data_forcing.F90 sourcefile~rdb_ocean_state.f90->sourcefile~rdb_ocean_data_forcing.f90 sourcefile~rdb_ocean_data_input.f90 rdb_ocean_data_input.F90 sourcefile~rdb_ocean_state.f90->sourcefile~rdb_ocean_data_input.f90 sourcefile~rdb_ocean_diag.f90 rdb_ocean_diag.F90 sourcefile~rdb_ocean_state.f90->sourcefile~rdb_ocean_diag.f90 sourcefile~rdb_ocean_state.f90->sourcefile~rdb_ocean_gm.f90 sourcefile~rdb_ocean_state.f90->sourcefile~rdb_ocean_hdiff_tracer.f90 sourcefile~rdb_ocean_state.f90->sourcefile~rdb_ocean_isopycnal_slopes.f90 sourcefile~rdb_ocean_state.f90->sourcefile~rdb_ocean_kappa_shear.f90 sourcefile~rdb_ocean_state.f90->sourcefile~rdb_ocean_meke.f90 sourcefile~rdb_ocean_state.f90->sourcefile~rdb_ocean_mle.f90 sourcefile~rdb_ocean_obc.f90 rdb_ocean_obc.F90 sourcefile~rdb_ocean_state.f90->sourcefile~rdb_ocean_obc.f90 sourcefile~rdb_ocean_state.f90->sourcefile~rdb_ocean_periodic.f90 sourcefile~rdb_ocean_state.f90->sourcefile~rdb_ocean_pseudo_salt.f90 sourcefile~rdb_ocean_state.f90->sourcefile~rdb_ocean_redi.f90 sourcefile~rdb_ocean_restart.f90 rdb_ocean_restart.F90 sourcefile~rdb_ocean_state.f90->sourcefile~rdb_ocean_restart.f90 sourcefile~rdb_ocean_restart_io.f90 rdb_ocean_restart_io.F90 sourcefile~rdb_ocean_state.f90->sourcefile~rdb_ocean_restart_io.f90 sourcefile~rdb_ocean_state.f90->sourcefile~rdb_ocean_tidal_mixing.f90 sourcefile~rdb_ocean_state.f90->sourcefile~rdb_ocean_varmix.f90 sourcefile~rdb_ocean_state.f90->sourcefile~rdb_ocean_vertical_advection.f90 sourcefile~rdb_ocean_state.f90->sourcefile~rdb_ocean_wave_speed.f90 sourcefile~rdb_ocean_z_init.f90 rdb_ocean_z_init.F90 sourcefile~rdb_ocean_state.f90->sourcefile~rdb_ocean_z_init.f90 sourcefile~rdb_ocean_state.f90->sourcefile~rdb_profiler.f90 sourcefile~rdb_ocean_surface_flux.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ocean_surface_flux.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_ocean_surface_flux.f90->sourcefile~rdb_mem_report.f90 sourcefile~rdb_ocean_surface_flux.f90->sourcefile~rdb_multilayer_state.f90 sourcefile~rdb_ocean_surface_stress.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ocean_surface_stress.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_ocean_surface_stress.f90->sourcefile~rdb_mem_report.f90 sourcefile~rdb_ocean_surface_stress.f90->sourcefile~rdb_multilayer_state.f90 sourcefile~rdb_ocean_surface_stress.f90->sourcefile~rdb_scratch_3d.f90 sourcefile~rdb_ocean_tide_astro.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ocean_tides.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ocean_tides.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_ocean_tides.f90->sourcefile~rdb_ocean_tide_astro.f90 sourcefile~rdb_ocean_tides.f90->sourcefile~rdb_mem_report.f90 sourcefile~rdb_ocean_top_drag.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ocean_top_drag.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_ocean_top_drag.f90->sourcefile~rdb_mem_report.f90 sourcefile~rdb_ocean_top_drag.f90->sourcefile~rdb_multilayer_state.f90 sourcefile~rdb_ocean_top_drag.f90->sourcefile~rdb_scratch_3d.f90 sourcefile~rdb_ocean_vcoord.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ocean_vcoord.f90->sourcefile~rdb_eos.f90 sourcefile~rdb_ocean_vcoord.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_ocean_vcoord.f90->sourcefile~rdb_vcoord.f90 sourcefile~rdb_ocean_vcoord.f90->sourcefile~rdb_mem_report.f90 sourcefile~rdb_ocean_vdiff.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ocean_vdiff.f90->sourcefile~rdb_eos.f90 sourcefile~rdb_ocean_vdiff.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_ocean_vdiff.f90->sourcefile~rdb_mem_report.f90 sourcefile~rdb_ocean_vdiff.f90->sourcefile~rdb_multilayer_state.f90 sourcefile~rdb_ocean_vdiff.f90->sourcefile~rdb_scratch_3d.f90 sourcefile~rdb_tracer.f90 rdb_tracer.F90 sourcefile~rdb_ocean_vdiff.f90->sourcefile~rdb_tracer.f90 sourcefile~rdb_ocean_vmix.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ocean_vmix.f90->sourcefile~rdb_eos.f90 sourcefile~rdb_ocean_vmix.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_ocean_vmix.f90->sourcefile~rdb_ocean_surface_flux.f90 sourcefile~rdb_ocean_vmix.f90->sourcefile~rdb_ocean_surface_stress.f90 sourcefile~rdb_ocean_vmix.f90->sourcefile~rdb_mem_report.f90 sourcefile~rdb_ocean_vmix.f90->sourcefile~rdb_multilayer_state.f90 sourcefile~rdb_recon_weno.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_recon_weno.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_vcoord.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_barotropic_coupling.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_barotropic_coupling.f90->sourcefile~rdb_coriolis_adv.f90 sourcefile~rdb_barotropic_coupling.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_barotropic_coupling.f90->sourcefile~rdb_ocean_bottom_drag.f90 sourcefile~rdb_barotropic_coupling.f90->sourcefile~rdb_ocean_boundary_types.f90 sourcefile~rdb_barotropic_coupling.f90->sourcefile~rdb_ocean_horizontal_viscosity.f90 sourcefile~rdb_barotropic_coupling.f90->sourcefile~rdb_ocean_metrics.f90 sourcefile~rdb_barotropic_coupling.f90->sourcefile~rdb_ocean_pressure_force.f90 sourcefile~rdb_barotropic_coupling.f90->sourcefile~rdb_ocean_surface_stress.f90 sourcefile~rdb_barotropic_coupling.f90->sourcefile~rdb_ocean_top_drag.f90 sourcefile~rdb_barotropic_coupling.f90->sourcefile~rdb_barotropic_workstate.f90 sourcefile~rdb_barotropic_coupling.f90->sourcefile~rdb_multilayer_state.f90 sourcefile~rdb_barotropic_state.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_barotropic_state.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_barotropic_state.f90->sourcefile~rdb_mem_report.f90 sourcefile~rdb_barotropic_substep.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_barotropic_substep.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_barotropic_substep.f90->sourcefile~rdb_ocean_boundary_types.f90 sourcefile~rdb_barotropic_substep.f90->sourcefile~rdb_ocean_fold_exchange.f90 sourcefile~rdb_barotropic_substep.f90->sourcefile~rdb_ocean_halo.f90 sourcefile~rdb_barotropic_substep.f90->sourcefile~rdb_ocean_metrics.f90 sourcefile~rdb_barotropic_substep.f90->sourcefile~rdb_barotropic_workstate.f90 sourcefile~rdb_barotropic_substep.f90->sourcefile~rdb_ocean_halo_counters.f90 sourcefile~rdb_barotropic_substep.f90->sourcefile~rdb_profiler.f90 sourcefile~rdb_bt_cont_type.f90 rdb_bt_cont_type.F90 sourcefile~rdb_barotropic_substep.f90->sourcefile~rdb_bt_cont_type.f90 sourcefile~rdb_barotropic_workstate.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_barotropic_workstate.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_barotropic_workstate.f90->sourcefile~rdb_mem_report.f90 sourcefile~rdb_bathymetry.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_bathymetry.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_bathymetry.f90->sourcefile~rdb_ocean_status.f90 sourcefile~rdb_bathymetry.f90->sourcefile~rdb_io_netcdf.f90 sourcefile~rdb_comm_env.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_continuity.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_continuity.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_continuity.f90->sourcefile~rdb_ocean_boundary_types.f90 sourcefile~rdb_continuity.f90->sourcefile~rdb_ocean_fold.f90 sourcefile~rdb_continuity.f90->sourcefile~rdb_ocean_fold_apply.f90 sourcefile~rdb_continuity.f90->sourcefile~rdb_ocean_fold_exchange.f90 sourcefile~rdb_continuity.f90->sourcefile~rdb_ocean_halo.f90 sourcefile~rdb_continuity.f90->sourcefile~rdb_ocean_metrics.f90 sourcefile~rdb_continuity.f90->sourcefile~rdb_recon_weno.f90 sourcefile~rdb_continuity.f90->sourcefile~rdb_barotropic_state.f90 sourcefile~rdb_continuity.f90->sourcefile~rdb_mem_report.f90 sourcefile~rdb_continuity.f90->sourcefile~rdb_multilayer_state.f90 sourcefile~rdb_continuity.f90->sourcefile~rdb_ocean_gm.f90 sourcefile~rdb_continuity.f90->sourcefile~rdb_ocean_mle.f90 sourcefile~rdb_continuity.f90->sourcefile~rdb_ocean_periodic.f90 sourcefile~rdb_continuity.f90->sourcefile~rdb_profiler.f90 sourcefile~rdb_continuity.f90->sourcefile~rdb_scratch_3d.f90 sourcefile~rdb_continuity.f90->sourcefile~rdb_tracer.f90 sourcefile~rdb_ice_enthalpy.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ice_init.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ice_init.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_ice_init.f90->sourcefile~rdb_ocean_metrics.f90 sourcefile~rdb_ice_init.f90->sourcefile~rdb_ice_enthalpy.f90 sourcefile~rdb_ice_init.f90->sourcefile~rdb_ice_state.f90 sourcefile~rdb_ice_init.f90->sourcefile~rdb_multilayer_state.f90 sourcefile~rdb_ice_column.f90 rdb_ice_column.F90 sourcefile~rdb_ice_init.f90->sourcefile~rdb_ice_column.f90 sourcefile~rdb_ice_state.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ice_state.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_ice_state.f90->sourcefile~rdb_ice_enthalpy.f90 sourcefile~rdb_ice_state.f90->sourcefile~rdb_mem_report.f90 sourcefile~rdb_ice_state.f90->sourcefile~rdb_ice_column.f90 sourcefile~rdb_io_netcdf.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_io_netcdf.f90->sourcefile~rdb_error_ring.f90 sourcefile~rdb_mem_report.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_multilayer_state.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_multilayer_state.f90->sourcefile~rdb_error_ring.f90 sourcefile~rdb_multilayer_state.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_multilayer_state.f90->sourcefile~rdb_efp.f90 sourcefile~rdb_multilayer_state.f90->sourcefile~rdb_mem_report.f90 sourcefile~rdb_multilayer_state.f90->sourcefile~rdb_tracer.f90 sourcefile~rdb_nml_schema.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_nml_schema.f90->sourcefile~rdb_error_ring.f90 sourcefile~rdb_ocean_bathymetry_inject.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ocean_bathymetry_inject.f90->sourcefile~rdb_error_ring.f90 sourcefile~rdb_ocean_bathymetry_inject.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_ocean_bathymetry_inject.f90->sourcefile~rdb_ocean_status.f90 sourcefile~rdb_ocean_bipolar.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ocean_bt_budget_probe.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ocean_bt_budget_probe.f90->sourcefile~rdb_coriolis_adv.f90 sourcefile~rdb_ocean_bt_budget_probe.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_ocean_bt_budget_probe.f90->sourcefile~rdb_ocean_bottom_drag.f90 sourcefile~rdb_ocean_bt_budget_probe.f90->sourcefile~rdb_ocean_horizontal_viscosity.f90 sourcefile~rdb_ocean_bt_budget_probe.f90->sourcefile~rdb_ocean_pressure_force.f90 sourcefile~rdb_ocean_bt_budget_probe.f90->sourcefile~rdb_ocean_surface_stress.f90 sourcefile~rdb_ocean_bt_budget_probe.f90->sourcefile~rdb_barotropic_workstate.f90 sourcefile~rdb_ocean_bt_budget_probe.f90->sourcefile~rdb_multilayer_state.f90 sourcefile~rdb_ocean_bt_wide.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ocean_bt_wide.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_ocean_bt_wide.f90->sourcefile~rdb_ocean_boundary_types.f90 sourcefile~rdb_ocean_bt_wide.f90->sourcefile~rdb_ocean_halo.f90 sourcefile~rdb_ocean_bt_wide.f90->sourcefile~rdb_ocean_metrics.f90 sourcefile~rdb_ocean_bt_wide.f90->sourcefile~rdb_barotropic_substep.f90 sourcefile~rdb_ocean_bt_wide.f90->sourcefile~rdb_barotropic_workstate.f90 sourcefile~rdb_ocean_bt_wide.f90->sourcefile~rdb_mem_report.f90 sourcefile~rdb_ocean_bt_wide.f90->sourcefile~rdb_profiler.f90 sourcefile~rdb_ocean_cavity_flux.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ocean_cavity_flux.f90->sourcefile~rdb_eos.f90 sourcefile~rdb_ocean_cavity_flux.f90->sourcefile~rdb_error_ring.f90 sourcefile~rdb_ocean_cavity_flux.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_ocean_cavity_flux.f90->sourcefile~rdb_halo.f90 sourcefile~rdb_ocean_cavity_flux.f90->sourcefile~rdb_ocean_cavity_melt.f90 sourcefile~rdb_ocean_cavity_flux.f90->sourcefile~rdb_ocean_metrics.f90 sourcefile~rdb_ocean_cavity_flux.f90->sourcefile~rdb_ocean_status.f90 sourcefile~rdb_ocean_cavity_flux.f90->sourcefile~rdb_ocean_surface_flux.f90 sourcefile~rdb_ocean_cavity_flux.f90->sourcefile~rdb_mem_report.f90 sourcefile~rdb_ocean_cavity_flux.f90->sourcefile~rdb_multilayer_state.f90 sourcefile~rdb_ocean_chksum.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ocean_chksum.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_ocean_chksum.f90->sourcefile~rdb_halo.f90 sourcefile~rdb_ocean_chksum.f90->sourcefile~rdb_barotropic_workstate.f90 sourcefile~rdb_ocean_chksum.f90->sourcefile~rdb_multilayer_state.f90 sourcefile~rdb_ocean_console_stats.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ocean_console_stats.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_ocean_console_stats.f90->sourcefile~rdb_halo.f90 sourcefile~rdb_ocean_console_stats.f90->sourcefile~rdb_ocean_metrics.f90 sourcefile~rdb_ocean_console_stats.f90->sourcefile~rdb_efp.f90 sourcefile~rdb_ocean_console_stats.f90->sourcefile~rdb_multilayer_state.f90 sourcefile~rdb_ocean_console_stats.f90->sourcefile~rdb_ocean_halo_counters.f90 sourcefile~rdb_console_stats.f90 rdb_console_stats.F90 sourcefile~rdb_ocean_console_stats.f90->sourcefile~rdb_console_stats.f90 sourcefile~rdb_ocean_console_stats.f90->sourcefile~rdb_ice_column.f90 sourcefile~rdb_ocean_data_forcing.f90->sourcefile~rdb_config.f90 sourcefile~rdb_ocean_data_forcing.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ocean_data_forcing.f90->sourcefile~rdb_error_ring.f90 sourcefile~rdb_ocean_data_forcing.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_ocean_data_forcing.f90->sourcefile~rdb_ocean_boundary_types.f90 sourcefile~rdb_ocean_data_forcing.f90->sourcefile~rdb_ocean_halo_state.f90 sourcefile~rdb_ocean_data_forcing.f90->sourcefile~rdb_ocean_status.f90 sourcefile~rdb_ocean_data_forcing.f90->sourcefile~rdb_ocean_surface_flux.f90 sourcefile~rdb_ocean_data_forcing.f90->sourcefile~rdb_ocean_surface_stress.f90 sourcefile~rdb_ocean_data_forcing.f90->sourcefile~rdb_ocean_data_input.f90 sourcefile~rdb_ocean_data_input.f90->sourcefile~rdb_config.f90 sourcefile~rdb_ocean_data_input.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ocean_data_input.f90->sourcefile~rdb_error_ring.f90 sourcefile~rdb_ocean_data_input.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_ocean_data_input.f90->sourcefile~rdb_ocean_status.f90 sourcefile~rdb_ocean_data_input.f90->sourcefile~rdb_io_netcdf.f90 sourcefile~rdb_ocean_data_input.f90->sourcefile~rdb_mem_report.f90 sourcefile~rdb_ocean_diag.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ocean_diag.f90->sourcefile~rdb_error_ring.f90 sourcefile~rdb_ocean_diag.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_ocean_diag.f90->sourcefile~rdb_ocean_status.f90 sourcefile~rdb_ocean_diag.f90->sourcefile~rdb_mem_report.f90 sourcefile~rdb_ocean_diag_mask.f90 rdb_ocean_diag_mask.F90 sourcefile~rdb_ocean_diag.f90->sourcefile~rdb_ocean_diag_mask.f90 sourcefile~rdb_ocean_eos_compute.f90->sourcefile~rdb_eos.f90 sourcefile~rdb_ocean_eos_compute.f90->sourcefile~rdb_multilayer_state.f90 sourcefile~rdb_ocean_ghost_poison.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ocean_ghost_poison.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_ocean_ghost_poison.f90->sourcefile~rdb_ocean_surface_stress.f90 sourcefile~rdb_ocean_ghost_poison.f90->sourcefile~rdb_barotropic_workstate.f90 sourcefile~rdb_ocean_ghost_poison.f90->sourcefile~rdb_multilayer_state.f90 sourcefile~rdb_ocean_gm.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ocean_gm.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_ocean_gm.f90->sourcefile~rdb_ocean_metrics.f90 sourcefile~rdb_ocean_gm.f90->sourcefile~rdb_mem_report.f90 sourcefile~rdb_ocean_gm.f90->sourcefile~rdb_multilayer_state.f90 sourcefile~rdb_ocean_gm.f90->sourcefile~rdb_ocean_isopycnal_slopes.f90 sourcefile~rdb_ocean_hdiff_tracer.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ocean_hdiff_tracer.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_ocean_hdiff_tracer.f90->sourcefile~rdb_ocean_boundary_types.f90 sourcefile~rdb_ocean_hdiff_tracer.f90->sourcefile~rdb_ocean_metrics.f90 sourcefile~rdb_ocean_hdiff_tracer.f90->sourcefile~rdb_mem_report.f90 sourcefile~rdb_ocean_hdiff_tracer.f90->sourcefile~rdb_multilayer_state.f90 sourcefile~rdb_ocean_hdiff_tracer.f90->sourcefile~rdb_scratch_3d.f90 sourcefile~rdb_ocean_hdiff_tracer.f90->sourcefile~rdb_tracer.f90 sourcefile~rdb_ocean_ideal_age.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ocean_ideal_age.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_ocean_ideal_age.f90->sourcefile~rdb_multilayer_state.f90 sourcefile~rdb_ocean_isopycnal_slopes.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ocean_isopycnal_slopes.f90->sourcefile~rdb_eos.f90 sourcefile~rdb_ocean_isopycnal_slopes.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_ocean_isopycnal_slopes.f90->sourcefile~rdb_ocean_metrics.f90 sourcefile~rdb_ocean_isopycnal_slopes.f90->sourcefile~rdb_mem_report.f90 sourcefile~rdb_ocean_isopycnal_slopes.f90->sourcefile~rdb_multilayer_state.f90 sourcefile~rdb_ocean_kappa_shear.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ocean_kappa_shear.f90->sourcefile~rdb_eos.f90 sourcefile~rdb_ocean_kappa_shear.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_ocean_kappa_shear.f90->sourcefile~rdb_mem_report.f90 sourcefile~rdb_ocean_kappa_shear.f90->sourcefile~rdb_multilayer_state.f90 sourcefile~rdb_massless.f90 rdb_massless.F90 sourcefile~rdb_ocean_kappa_shear.f90->sourcefile~rdb_massless.f90 sourcefile~rdb_ocean_ke_probe.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ocean_ke_probe.f90->sourcefile~rdb_coriolis_adv.f90 sourcefile~rdb_ocean_ke_probe.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_ocean_ke_probe.f90->sourcefile~rdb_ocean_metrics.f90 sourcefile~rdb_ocean_ke_probe.f90->sourcefile~rdb_multilayer_state.f90 sourcefile~rdb_ocean_meke.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ocean_meke.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_ocean_meke.f90->sourcefile~rdb_ocean_metrics.f90 sourcefile~rdb_ocean_meke.f90->sourcefile~rdb_mem_report.f90 sourcefile~rdb_ocean_meke.f90->sourcefile~rdb_multilayer_state.f90 sourcefile~rdb_ocean_meke.f90->sourcefile~rdb_ocean_gm.f90 sourcefile~rdb_ocean_meke.f90->sourcefile~rdb_ocean_varmix.f90 sourcefile~rdb_ocean_meke.f90->sourcefile~rdb_ocean_wave_speed.f90 sourcefile~rdb_ocean_min_thickness.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ocean_min_thickness.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_ocean_min_thickness.f90->sourcefile~rdb_multilayer_state.f90 sourcefile~rdb_remap_column.f90 rdb_remap_column.F90 sourcefile~rdb_ocean_min_thickness.f90->sourcefile~rdb_remap_column.f90 sourcefile~rdb_ocean_mle.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ocean_mle.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_ocean_mle.f90->sourcefile~rdb_ocean_boundary_types.f90 sourcefile~rdb_ocean_mle.f90->sourcefile~rdb_ocean_epbl.f90 sourcefile~rdb_ocean_mle.f90->sourcefile~rdb_ocean_metrics.f90 sourcefile~rdb_ocean_mle.f90->sourcefile~rdb_ocean_surface_stress.f90 sourcefile~rdb_ocean_mle.f90->sourcefile~rdb_mem_report.f90 sourcefile~rdb_ocean_mle.f90->sourcefile~rdb_multilayer_state.f90 sourcefile~rdb_ocean_obc.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ocean_obc.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_ocean_obc.f90->sourcefile~rdb_mem_report.f90 sourcefile~rdb_ocean_obc_baroclinic.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ocean_obc_baroclinic.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_ocean_obc_baroclinic.f90->sourcefile~rdb_ocean_boundary_types.f90 sourcefile~rdb_ocean_obc_baroclinic.f90->sourcefile~rdb_barotropic_workstate.f90 sourcefile~rdb_ocean_obc_baroclinic.f90->sourcefile~rdb_multilayer_state.f90 sourcefile~rdb_ocean_periodic.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ocean_periodic.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_ocean_periodic.f90->sourcefile~rdb_ocean_boundary_types.f90 sourcefile~rdb_ocean_periodic.f90->sourcefile~rdb_multilayer_state.f90 sourcefile~rdb_ocean_pgf_reconstruct.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ocean_pgf_reconstruct.f90->sourcefile~rdb_eos.f90 sourcefile~rdb_ocean_pseudo_salt.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ocean_pseudo_salt.f90->sourcefile~rdb_error_ring.f90 sourcefile~rdb_ocean_pseudo_salt.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_ocean_pseudo_salt.f90->sourcefile~rdb_multilayer_state.f90 sourcefile~rdb_ocean_redi.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ocean_redi.f90->sourcefile~rdb_eos.f90 sourcefile~rdb_ocean_redi.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_ocean_redi.f90->sourcefile~rdb_ocean_boundary_types.f90 sourcefile~rdb_ocean_redi.f90->sourcefile~rdb_ocean_metrics.f90 sourcefile~rdb_ocean_redi.f90->sourcefile~rdb_mem_report.f90 sourcefile~rdb_ocean_redi.f90->sourcefile~rdb_multilayer_state.f90 sourcefile~rdb_ocean_redi.f90->sourcefile~rdb_tracer.f90 sourcefile~rdb_ocean_remap.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ocean_remap.f90->sourcefile~rdb_eos.f90 sourcefile~rdb_ocean_remap.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_ocean_remap.f90->sourcefile~rdb_ocean_vcoord.f90 sourcefile~rdb_ocean_remap.f90->sourcefile~rdb_multilayer_state.f90 sourcefile~rdb_ocean_remap.f90->sourcefile~rdb_tracer.f90 sourcefile~rdb_ocean_remap.f90->sourcefile~rdb_remap_column.f90 sourcefile~rdb_ocean_restart.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ocean_restart_io.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ocean_restart_io.f90->sourcefile~rdb_decomp.f90 sourcefile~rdb_ocean_restart_io.f90->sourcefile~rdb_error_ring.f90 sourcefile~rdb_ocean_restart_io.f90->sourcefile~rdb_ocean_status.f90 sourcefile~rdb_ocean_restart_io.f90->sourcefile~rdb_io_netcdf.f90 sourcefile~rdb_ocean_restart_io.f90->sourcefile~rdb_ocean_restart.f90 sourcefile~rdb_ocean_tidal_mixing.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ocean_tidal_mixing.f90->sourcefile~rdb_eos.f90 sourcefile~rdb_ocean_tidal_mixing.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_ocean_tidal_mixing.f90->sourcefile~rdb_mem_report.f90 sourcefile~rdb_ocean_tidal_mixing.f90->sourcefile~rdb_multilayer_state.f90 sourcefile~rdb_ocean_varmix.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ocean_varmix.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_ocean_varmix.f90->sourcefile~rdb_ocean_metrics.f90 sourcefile~rdb_ocean_varmix.f90->sourcefile~rdb_mem_report.f90 sourcefile~rdb_ocean_varmix.f90->sourcefile~rdb_multilayer_state.f90 sourcefile~rdb_ocean_varmix.f90->sourcefile~rdb_ocean_isopycnal_slopes.f90 sourcefile~rdb_ocean_varmix.f90->sourcefile~rdb_ocean_wave_speed.f90 sourcefile~rdb_ocean_vertical_advection.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ocean_vertical_advection.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_ocean_vertical_advection.f90->sourcefile~rdb_mem_report.f90 sourcefile~rdb_ocean_vertical_advection.f90->sourcefile~rdb_multilayer_state.f90 sourcefile~rdb_ocean_vertical_advection.f90->sourcefile~rdb_scratch_3d.f90 sourcefile~rdb_ocean_vertical_advection.f90->sourcefile~rdb_tracer.f90 sourcefile~rdb_ocean_wave_speed.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ocean_wave_speed.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_ocean_wave_speed.f90->sourcefile~rdb_ocean_metrics.f90 sourcefile~rdb_ocean_wave_speed.f90->sourcefile~rdb_mem_report.f90 sourcefile~rdb_ocean_wave_speed.f90->sourcefile~rdb_multilayer_state.f90 sourcefile~rdb_ocean_z_init.f90->sourcefile~rdb_config.f90 sourcefile~rdb_ocean_z_init.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ocean_z_init.f90->sourcefile~rdb_error_ring.f90 sourcefile~rdb_ocean_z_init.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_ocean_z_init.f90->sourcefile~rdb_ocean_status.f90 sourcefile~rdb_ocean_z_init.f90->sourcefile~rdb_io_netcdf.f90 sourcefile~rdb_ocean_z_init.f90->sourcefile~rdb_multilayer_state.f90 sourcefile~rdb_scratch_3d.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_scratch_3d.f90->sourcefile~rdb_mem_report.f90 sourcefile~rdb_tracer.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_tracer.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_tracer.f90->sourcefile~rdb_mem_report.f90 sourcefile~rdb_bt_cont_type.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_bt_cont_type.f90->sourcefile~rdb_barotropic_workstate.f90 sourcefile~rdb_console_stats.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_console_stats.f90->sourcefile~rdb_efp.f90 sourcefile~rdb_ice_column.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ice_column.f90->sourcefile~rdb_ice_enthalpy.f90 sourcefile~rdb_ice_mass.f90 rdb_ice_mass.F90 sourcefile~rdb_ice_column.f90->sourcefile~rdb_ice_mass.f90 sourcefile~rdb_ice_optics.f90 rdb_ice_optics.F90 sourcefile~rdb_ice_column.f90->sourcefile~rdb_ice_optics.f90 sourcefile~rdb_massless.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ocean_diag_mask.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ocean_diag_mask.f90->sourcefile~rdb_grid.f90 sourcefile~rdb_ocean_diag_mask.f90->sourcefile~rdb_mem_report.f90 sourcefile~rdb_remap_column.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ice_mass.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ice_mass.f90->sourcefile~rdb_ice_enthalpy.f90 sourcefile~rdb_ice_optics.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ice_optics.f90->sourcefile~rdb_ice_enthalpy.f90

Files dependent on this one

sourcefile~~rdb_ocean_setup.f90~~AfferentGraph sourcefile~rdb_ocean_setup.f90 rdb_ocean_setup.F90 sourcefile~rdb_ocean_engine.f90 rdb_ocean_engine.F90 sourcefile~rdb_ocean_engine.f90->sourcefile~rdb_ocean_setup.f90 sourcefile~rdb_driver.f90 rdb_driver.F90 sourcefile~rdb_driver.f90->sourcefile~rdb_ocean_engine.f90 sourcefile~rdb_handle.f90 rdb_handle.F90 sourcefile~rdb_handle.f90->sourcefile~rdb_ocean_engine.f90 sourcefile~rdb_ocean_api.f90 rdb_ocean_api.F90 sourcefile~rdb_ocean_api.f90->sourcefile~rdb_ocean_engine.f90 sourcefile~rdb_ocean_api.f90->sourcefile~rdb_handle.f90

Source Code

!! Ocean slot configuration from a parsed namelist config.
module rdb_ocean_setup
   !! Applies the per-concern `&ocean_*` namelist knobs onto an
   !! `ocean_state_t`'s kernel slots (bottom drag, vmix, lateral
   !! viscosity, vertical coord, PGF, barotropic split).  Pure
   !! cfg → slot wiring + rank-0 logging — no I/O, no NetCDF — so it
   !! is shared by the production driver and the NetCDF-free benchmark.
   !!
   !! Call order after `ocean_state%init_from_config` +
   !! `ocean_state_seed_from_cfg` + `register_default_tracers`:
   !!   configure_ocean_drag / _vmix / _lateral / _pgf / _bt / _bt_split
#ifdef LFORTRAN_PASSING
   use rdb_constants, only: wp, GRAVITY, LAND_DEPTH_THRESHOLD
#else
   use rdb_constants, only: wp, GRAVITY, LAND_DEPTH_THRESHOLD, NZ_STACK_MAX
#endif
   use rdb_constants, only: H_VANISHED, VCOORD_ZSTAR_FULL, VCOORD_ZSTAR
   use rdb_constants, only: FRHAT_ARITHMETIC, FRHAT_HYBRID, parse_frhat_scheme
   use rdb_config, only: config_t, ocean_bt_forcing_visc_rem_on, &
                         ocean_bt_renorm_visc_rem_on, ocean_bt_rem_from_visc_rem_on, &
                         ocean_bt_visc_rem_producer_on
   use rdb_grid, only: hgrid_t
   use rdb_decomp, only: decomp_t
   use rdb_ocean_state, only: ocean_state_t, ocean_state_seed_land_cells, &
                              pgf_nonoverlap_gate_on
   use rdb_ocean_pressure_force, only: OPGF_VARIANT_GPRIME, OPGF_VARIANT_FV_MOM6, &
                                       OPGF_VARIANT_FV_WRIGHT, parse_opgf_variant, &
                                       nonoverlap_vanish_tol_for
   use rdb_eos, only: parse_eos_variant, eos_validate, &
                      EOS_VARIANT_ROQUET_SPV, &
                      parse_tfreeze_set, eos_apply_tfreeze_set, &
                      TFREEZE_SET_SEAICE
   use rdb_ocean_vcoord, only: parse_ocean_vcoord_type, VCOORD_EULERIAN_Z, &
                               VCOORD_RHO, VCOORD_HYCOM, VCOORD_LAGRANGIAN, &
                               VCOORD_Z_FIXED, ocean_vcoord_z_fixed_target, &
                               ocean_vcoord_closed_face_masks, &
                               ocean_vcoord_eta0_target, &
                               ocean_vcoord_k_top_from_target, &
                               ocean_vcoord_k_bot_from_target, &
                               ocean_vcoord_set_z_fixed_profile, &
                               ocean_vcoord_count_ledges, &
                               ocean_vcoord_count_bed_steps, ocean_vcoord_t
   use rdb_vcoord, only: parse_remap_method, parse_z_fixed_profile, z_fixed_nominal_dz, &
                         ZFIXED_PROFILE_UNIFORM, ZFIXED_PROFILE_INVALID, ZFIXED_DZ_OK
   use rdb_ocean_bottom_drag, only: parse_bdrag_variant
   use rdb_ocean_vdiff, only: vdiff_bbl_configure, BBL_FORM_LINEAR, BBL_FORM_QUADRATIC
   use rdb_ocean_top_drag, only: parse_tdrag_variant, TDRAG_QUADRATIC, &
                                 top_drag_fill_face_cover_impl
   use rdb_ocean_lateral_mix, only: parse_lateral_closure, LMIX_NONE, &
                                    LMIX_LEITH, LMIX_SMAGORINSKY, &
                                    LMIX_BIHARMONIC, LMIX_LEITH_BIHARM
   use rdb_ocean_horizontal_viscosity, only: ocean_hvisc_set_aniso_direction
   use rdb_ocean_surface_stress, only: ocean_surface_stress_set_derived, &
                                       ocean_surface_stress_apply_cover
   use rdb_ocean_surface_flux, only: ocean_surface_flux_apply_cover_const
   use rdb_coriolis_adv, only: parse_pv_variant, PV_VARIANT_SADOURNY_HK, &
                               CORNER_H_CELL_MEAN, CORNER_H_MOM6_AREA, &
                               parse_pv_adv_scheme
   use rdb_ocean_boundary_types, only: ocean_bc_type_from_string, &
                                       ocean_bc_validate_periodic, &
                                       ocean_bc_validate_fold, &
                                       OBC_WALL, OBC_PERIODIC, OBC_OPEN, OBC_TIDAL, &
                                       OBC_CHAPMAN, OBC_NESTED, OBC_CLAMPED, OBC_SPONGE, &
                                       OBC_TRIPOLAR_FOLD, ocean_bc_outer_face_tag, &
                                       OBC_MAX_TIDAL_CONSTITUENTS, &
                                       ocean_bc_face_tag_t, &
                                       obc_tide_nodal_fill, obc_match_constituent
   use rdb_ocean_sponge, only: sponge_band_alpha, SPONGE_RAMP_COSINE, SPONGE_RAMP_LINEAR
   use rdb_ocean_halo, only: ocean_halo_centre, ocean_halo_is_decomposed, &
                             ocean_halo_is_decomposed_x, ocean_halo_is_decomposed_y, &
                             ocean_halo_face_x, ocean_halo_face_y, ocean_halo_is_init
   use rdb_ocean_halo_state, only: ocean_seam_refresh_surface_stress
   use rdb_ocean_fold_apply, only: ocean_fold_wrap_eta_2d
   use rdb_ocean_fold_exchange, only: ocean_fold_is_distributed
   use rdb_ocean_metrics, only: metrics_finalize, metrics_fill_cartesian, &
                                metrics_fill_spherical, metrics_fill_from_supergrid, &
                                metrics_fill_tripolar, metrics_apply_land_mask, &
                                metrics_fill_coriolis, parse_grid_config, &
                                parse_coriolis_scheme, ocean_metrics_t, &
                                GRID_CONFIG_CARTESIAN, GRID_CONFIG_SPHERICAL, &
                                GRID_CONFIG_SUPERGRID, GRID_CONFIG_TRIPOLAR, &
                                metrics_porous_alloc, metrics_closed_faces_alloc
   use rdb_ocean_cavity, only: cavity_fill_p_ice_ref, &
                               cavity_datum_impl, cavity_datum_residual
   use rdb_ocean_cavity_melt, only: CAVITY_GAMMA_RATIO_ISOMIP
   use rdb_ocean_porous, only: parse_porous_source, parse_porous_eta_interp, &
                               porous_fill_stats_resolved, &
                               porous_stats_are_ordered, &
                               POROUS_SOURCE_RESOLVED, POROUS_SOURCE_FILE
   use rdb_ocean_epbl, only: parse_epbl_mstar_scheme, parse_epbl_vstar_scheme, &
                             parse_epbl_combine, parse_epbl_lt_scheme
   use rdb_ocean_vmix, only: parse_kpp_sw_method, vmix_resolve_kd_min, &
                             bkgnd_henyey_conflicts_profile, &
                             parse_buoyancy_coeffs, BUOY_COEFFS_INVALID
   use rdb_ocean_geothermal, only: ocean_geothermal_t
   use rdb_ocean_fold, only: fold_north_corner
   use rdb_ocean_tides, only: tides_configure_astronomy, tides_build_struct
   use rdb_ocean_p_surf, only: p_surf_configure
   use rdb_ocean_tide_astro, only: tide_name_index, days_since_1900, &
                                   parse_date_string, TIDES_CATALOG_SIZE, &
                                   equilibrium_arguments, nodal_fu, TIDE_NAME
   use rdb_ocean_dyn, only: ocean_dt_tracer_advect_ratios_ok, &
                            SPLIT_SCHEME_SSP_RK2, SPLIT_SCHEME_PRED_CORR
   use rdb_recon_weno, only: parse_tracer_recon, TRACER_RECON_PPM
   use rdb_halo, only: halo_allreduce_min, halo_allreduce_sum
   use rdb_ocean_status, only: OCEAN_STATUS_OK, OCEAN_STATUS_ERR_SETUP
   use rdb_error_ring, only: fail
   use pic_logger, only: logger => global_logger
   use pic_strings, only: to_string
   implicit none
   private

#ifdef LFORTRAN_PASSING
   integer, parameter :: NZ_STACK_MAX = 64
      !! LFortran 0.64 workaround: module-local copy of the rdb_constants value
      !! (an imported parameter used as an explicit-shape dummy bound inside a
      !! PURE call becomes an impure getter under LFortran). Keep in sync (=64).
#endif

   public :: configure_ocean_metrics
   public :: configure_ocean_land_mask
   public :: configure_ocean_forcing
   public :: configure_ocean_drag
   public :: configure_ocean_hdiff
   public :: configure_ocean_vmix
   public :: configure_ocean_tracers
   public :: configure_ocean_lateral
   public :: configure_ocean_reference_density
   public :: configure_ocean_pgf
   public :: configure_ocean_bt
   public :: configure_ocean_bt_split
   public :: configure_ocean_tides
   public :: configure_ocean_p_surf
   public :: configure_ocean_wave_drag
   public :: wave_drag_roughness_proxy
   public :: configure_ocean_porous
   public :: configure_ocean_closed_faces
   public :: configure_ocean_k_top
   public :: configure_ocean_k_bot
   public :: configure_ocean_z_fixed_profile
   public :: configure_ocean_cavity
   public :: configure_ocean_cavity_melt
   public :: configure_ocean_top_drag
   public :: cavity_resolve_gamma_s
   public :: cavity_count_zero_f
   public :: cavity_count_unloaded_p_top
   public :: configure_ocean_wetdry
   public :: bt_auto_n_inner
   public :: metrics_bt_cfl_length
   public :: bt_auto_n_inner_from_dt
   public :: bt_cfl_dt_wet
   public :: configure_ocean_bc
   public :: configure_ocean_sponge

contains

   subroutine configure_ocean_metrics(cfg, ocean_state, grid, compute_rank, ierr)
      !! Fill the `ocean_metrics_t` slot per `cfg%ocean%grid%grid_config`,
      !! then single-source the inverses + hvisc ratio bundle
      !! (`metrics_finalize`).  Must run BEFORE `ocean_state_enter_data`
      !! (the host fill is what the GPU copyin captures).  For
      !! "spherical", the `&grid_nml dx`/`dy` are reinterpreted as
      !! dlon/dlat in degrees.
      type(config_t), intent(in) :: cfg
      type(ocean_state_t), intent(inout) :: ocean_state
      type(hgrid_t), intent(in) :: grid
      integer, intent(in) :: compute_rank
      integer, intent(out), optional :: ierr
         !! Non-zero on a grid/geometry configuration failure when present;
         !! absent behaves as today (`error stop`).

      integer :: gc, local_ierr

      gc = parse_grid_config(cfg%ocean%grid%grid_config)
      select case (gc)
      case (GRID_CONFIG_SPHERICAL)
         ! Validation the schema can't express (cross-knob domain check).
         if (cfg%ocean%grid%rad_earth <= 0.0_wp) then
            call fail("configure_ocean_metrics: spherical grid needs rad_earth > 0", ierr, OCEAN_STATUS_ERR_SETUP)
            return
         end if
         ! Include ghost rows: the generator fills ghost rows beyond the physical
         ! edge by formula (see metrics_fill_spherical), so the most extreme
         ! latitude reached is lat_south - (nghost+0.5)*dlat (south ghost centre)
         ! and lat_south + (ny_phys+nghost+0.5)*dlat (north ghost centre).
         ! Near-pole sectors need tripolar treatment (M3 follow-up).
         if (cfg%ocean%grid%lat_south - (real(grid%nghost, wp) + 0.5_wp)*grid%dy < -90.0_wp .or. &
             cfg%ocean%grid%lat_south + real(grid%ny_phys + grid%nghost, wp)*grid%dy + &
             0.5_wp*grid%dy > 90.0_wp) then
            call fail("configure_ocean_metrics: spherical domain (including ghost rows) "// &
                      "exceeds |lat| <= 90. Near-pole sectors need tripolar treatment (M3).", ierr, OCEAN_STATUS_ERR_SETUP)
            return
         end if
         ! grid%dx / grid%dy are dlon / dlat in DEGREES here.
         call metrics_fill_spherical(ocean_state%metrics, grid, &
                                     cfg%ocean%grid%lon_west, cfg%ocean%grid%lat_south, &
                                     grid%dx, grid%dy, cfg%ocean%grid%rad_earth)
         if (compute_rank == 0) then
            call logger%info("Grid config:      spherical lon-lat sector")
         end if
      case (GRID_CONFIG_SUPERGRID)
         if (len_trim(cfg%ocean%grid%supergrid_file) == 0) then
            call fail("configure_ocean_metrics: grid_config='supergrid' "// &
                      "requires supergrid_file to be set in &ocean_grid_nml", ierr, OCEAN_STATUS_ERR_SETUP)
            return
         end if
         ! ierr threaded down ONLY when THIS routine's own ierr is
         ! present: otherwise metrics_fill_from_supergrid must keep
         ! reaching its own `error stop` (specific dimension-mismatch /
         ! NetCDF-failure text) rather than the generic wrapper message
         ! below (P0.1 review F2).
         !
         ! The edge tags decide the ghost-metric topology (periodic-x wrap,
         ! tripolar fold) exactly as they do for the analytic tripolar; the
         ! reader cross-checks the fold against the file's own top row.
         if (present(ierr)) then
            call metrics_fill_from_supergrid(ocean_state%metrics, grid, &
                                             cfg%ocean%grid%supergrid_file, ierr=local_ierr, &
                                             periodic_x=tags_periodic_x(cfg), &
                                             north_fold=tags_north_fold(cfg))
            if (local_ierr /= 0) then
               ierr = local_ierr
               return
            end if
         else
            call metrics_fill_from_supergrid(ocean_state%metrics, grid, &
                                             cfg%ocean%grid%supergrid_file, &
                                             periodic_x=tags_periodic_x(cfg), &
                                             north_fold=tags_north_fold(cfg))
         end if
         if (compute_rank == 0) then
            call logger%info("Grid config:      supergrid (mosaic) from "// &
                             trim(cfg%ocean%grid%supergrid_file))
         end if
      case (GRID_CONFIG_TRIPOLAR)
         ! Cross-knob domain checks the schema can't express.
         if (cfg%ocean%grid%rad_earth <= 0.0_wp) then
            call fail("configure_ocean_metrics: tripolar grid needs rad_earth > 0", ierr, OCEAN_STATUS_ERR_SETUP)
            return
         end if
         if (cfg%ocean%grid%phi_join <= cfg%ocean%grid%lat_south) then
            call fail("configure_ocean_metrics: tripolar phi_join must be "// &
                      "north of lat_south (the cap sits above the lon-lat region)", ierr, OCEAN_STATUS_ERR_SETUP)
            return
         end if
         ! Tripolar requires an east-west PERIODIC grid: the i-direction
         ! wraps the full 360 deg of pseudo-longitude and the north fold
         ! identifies the two halves of the top row.  Without periodic-x
         ! the wrap + fold exchange (M4c) is ill-defined.
         if (trim(cfg%ocean%bc%west) /= "periodic" .or. &
             trim(cfg%ocean%bc%east) /= "periodic") then
            call fail("configure_ocean_metrics: grid_config='tripolar' requires "// &
                      "periodic east-west BCs (&ocean_bc_nml west='periodic' east='periodic')", ierr, OCEAN_STATUS_ERR_SETUP)
            return
         end if
         ! A tripolar grid MUST close the top with the north fold; otherwise
         ! the duplicated top-row DOFs are never reconciled and the cap
         ! diverges.  (The reverse — fold tag requires a tripolar grid — is
         ! enforced by ocean_bc_validate_fold's periodic-w/e requirement plus
         ! this check.)
         if (trim(cfg%ocean%bc%north) /= "tripolar_fold") then
            call fail("configure_ocean_metrics: grid_config='tripolar' requires "// &
                      "north='tripolar_fold' (&ocean_bc_nml north='tripolar_fold')", ierr, OCEAN_STATUS_ERR_SETUP)
            return
         end if
         ! grid%dx / grid%dy are dlon / dlat in DEGREES here (as spherical).
         call metrics_fill_tripolar(ocean_state%metrics, grid, &
                                    cfg%ocean%grid%lon_west, cfg%ocean%grid%lat_south, &
                                    grid%dx, grid%dy, cfg%ocean%grid%rad_earth, &
                                    cfg%ocean%grid%phi_join, cfg%ocean%grid%lon_pole)
         if (compute_rank == 0) then
            call logger%info("Grid config:      tripolar (Murray 1996), phi_join="// &
                             to_string(cfg%ocean%grid%phi_join)//" lon_pole="// &
                             to_string(cfg%ocean%grid%lon_pole))
         end if
      case default   ! GRID_CONFIG_CARTESIAN
         call metrics_fill_cartesian(ocean_state%metrics, grid, grid%dx, grid%dy)
      end select

      call metrics_finalize(ocean_state%metrics)
      if (present(ierr)) ierr = OCEAN_STATUS_OK
   end subroutine configure_ocean_metrics

   pure function tags_periodic_x(cfg) result(per_x)
      !! `.true.` iff `&ocean_bc_nml` tags BOTH west and east `periodic` —
      !! the rule `ocean_bc_state_init` applies, for callers that run before
      !! `configure_ocean_bc` (the grid metrics).
      type(config_t), intent(in) :: cfg
      logical :: per_x
      per_x = ocean_bc_type_from_string(cfg%ocean%bc%west) == OBC_PERIODIC .and. &
              ocean_bc_type_from_string(cfg%ocean%bc%east) == OBC_PERIODIC
   end function tags_periodic_x

   pure function tags_north_fold(cfg) result(fold)
      !! `.true.` iff `&ocean_bc_nml north = "tripolar_fold"`.
      type(config_t), intent(in) :: cfg
      logical :: fold
      fold = ocean_bc_type_from_string(cfg%ocean%bc%north) == OBC_TRIPOLAR_FOLD
   end function tags_north_fold

   subroutine configure_ocean_land_mask(cfg, ocean_state, grid, compute_rank, warm_restart)
      !! Derive the static C-grid land masks from the seeded T-cell
      !! `wet_mask` and zero the 6 face metrics at land faces
      !! (`metrics_apply_land_mask`).  Run AFTER `configure_ocean_metrics`
      !! (the metrics + inverses must exist) AND `configure_ocean_bc` (the
      !! periodic / fold flags drive the halo-aware mask derivation), but
      !! BEFORE `ocean_state_enter_data` (the host edit is what the GPU
      !! copyin captures).
      !!
      !! For a domain with no land (`wet_mask≡1`, the flat-bottom /
      !! analytical path) every mask is 1.0 and the metric multiply is a
      !! literal no-op ⇒ bit-identical to a no-mask build — except that a
      !! face of ZERO width (`dy_cu`/`dx_cv = 0`, a node-aligned tripolar
      !! cap's pole columns) is closed even between wet cells.  This call
      !! is the only place that closure happens, so every geometry path
      !! must run it.
      type(config_t), intent(in) :: cfg
      type(ocean_state_t), intent(inout) :: ocean_state
      type(hgrid_t), intent(in) :: grid
      integer, intent(in) :: compute_rank
      logical, intent(in), optional :: warm_restart
         !! `.true.` when the prognostic state was just read from a
         !! checkpoint: the land-state seed below is then SKIPPED (the
         !! masks and metrics are still derived).  Absent ⇒ cold start.

      logical :: seed_land

      ! Multi-rank seam ghost fill of wet_mask (O3 land x decomp): a subdomain
      ! seam that bisects a continent needs the NEIGHBOUR rank's wet_T in the
      ! seam ghost columns so the C-grid face/corner masks derived inside
      ! metrics_apply_land_mask close the seam faces correctly.  Done HERE (the
      ! caller) rather than inside metrics_apply_land_mask because importing the
      ! comm-layer rdb_ocean_halo into the low-level rdb_ocean_metrics leaf
      ! creates an NVFORTRAN USE cycle (config -> lateral_mix -> metrics).
      ! ocean_halo_init has already run (driver orders it before this call).
      ! Gated on ocean_halo_is_DECOMPOSED (px>1 or py>1) — a genuine
      ! cross-rank seam is the ONLY case wet_mask needs a message: the
      ! periodic ghost wrap is done locally inside metrics_apply_land_mask,
      ! and a single-rank non-periodic run's ghosts are domain-boundary
      ! walls.  Gating on the seam (not periodicity / not merely is_init)
      ! also keeps the MPI runtime untouched in non-MPI ocean unit tests
      ! that init the halo via this setup path but never call MPI_Init
      ! (those are px=py=1 ⇒ skip).  Runs host-side before enter_data; a
      ! GPU+MPI init-time host-halo variant is an O3+ follow-up.
      if (ocean_halo_is_decomposed()) then
         call ocean_halo_centre(ocean_state%multilayer%wet_mask, device_resident=.false.)
      end if
      ! Distributed tripolar fold (px > 1, plan site S2):
      ! `metrics_apply_land_mask` folds its working copy with the LOCAL
      ! kernel, which is exact only on a tile holding the whole fold row.
      ! Fold the STORED wet mask here instead, through the owner-routed
      ! exchange (collective over the north rank row, host mode), and tell
      ! the land mask not to fold again.  Folding the stored mask, not a
      ! copy, also closes the seed-time gap for it: the seed derives
      ! `wet_mask` elementwise from the water column, whose north ghosts
      ! the single-rank seed folded first (`seed_wrap_static_2d`) and the
      ! px > 1 seed could not, so the stored ghosts now equal the serial
      ! run's bit for bit (an elementwise map commutes with the fold copy).
      if (ocean_fold_is_distributed()) then
         call ocean_fold_wrap_eta_2d(grid, ocean_state%bc, ocean_state%multilayer%wet_mask, &
                                     device_resident=.false.)
      end if

      ! Solid-wall velocity masking (&ocean_bc_nml mask_wall_velocity):
      ! thread the per-edge WALL flags so only genuine solid walls get their
      ! ghost wet_T zeroed (periodic + open/OBC edges keep their ghosts).
      !
      ! The `has_*` conjunct is load-bearing under MPI.  `bc_type` is the
      ! GLOBAL edge tag, identical on every rank; `has_*` is the per-rank
      ! "I actually sit on that domain edge" flag (set from decomp).
      ! Without the AND, every rank zeroed its own west/east/south/north
      ! ghost band, so an interior MPI SEAM was masked as land: wet_u at
      ! the seam face went to 0 and the six masked face metrics with it.
      ! Measured on double_gyre_mom6 (44x40, 10 days, wall basin): px=1
      ! gave En 3.036E-03 / MaxCFL 0.01000, px=2 gave 2.998E-03 / 0.01197
      ! — and MaxCFL is a max reduction, which is exactly order-invariant,
      ! so that spread could not have been reduction roundoff.  With the
      ! conjunct, px=1 and px=2 agree.
      ! The periodic flags select a LOCAL wrap of the working wet mask's
      ! ghosts, which is right only on an axis this rank holds whole: on a
      ! decomposed periodic axis the wrap ghosts are an MPI seam the
      ! exchange above already filled, and a local wrap overwrote them with
      ! the tile's own opposite edge (every rank of a px >= 3 periodic run
      ! with land near a seam then masked the wrong ghost faces; the
      ! decomposed-vs-serial bit-identity test caught it on 4x1).  The wall
      ! flags below carry the edge tags, so nothing else reads these two.
      call metrics_apply_land_mask(ocean_state%metrics, &
                                   ocean_state%multilayer%wet_mask, grid, &
                                   ocean_state%bc%periodic_x .and. .not. ocean_halo_is_decomposed_x(), &
                                   ocean_state%bc%periodic_y .and. .not. ocean_halo_is_decomposed_y(), &
                                   ocean_state%bc%north_fold .and. .not. ocean_fold_is_distributed(), &
                                   mask_wall_velocity=cfg%ocean%bc%mask_wall_velocity, &
                                   wall_west=(ocean_bc_outer_face_tag(ocean_state%bc%west%bc_type) == OBC_WALL &
                                              .and. ocean_state%bc%has_west), &
                                   wall_east=(ocean_bc_outer_face_tag(ocean_state%bc%east%bc_type) == OBC_WALL &
                                              .and. ocean_state%bc%has_east), &
                                   wall_south=(ocean_bc_outer_face_tag(ocean_state%bc%south%bc_type) == OBC_WALL &
                                               .and. ocean_state%bc%has_south), &
                                   wall_north=(ocean_bc_outer_face_tag(ocean_state%bc%north%bc_type) == OBC_WALL &
                                               .and. ocean_state%bc%has_north))

      ! Hold land T-cells at finite reference values so masked arithmetic
      ! never multiplies 0 by a NaN (0*NaN = NaN).  Land h_layer is floored
      ! to H_VANISHED (never 0), tracers held at the IC reference, layer +
      ! face velocities zeroed on land.
      !
      ! Cold start only.  The seed is the land state's INITIAL value, not an
      ! invariant the step maintains: the ALE remap regrids a land column
      ! like any other (a `z_fixed` land column leaves step 1 as `nz-1`
      ! `zstar_h_min` fillers over a bed cell holding the rest of its
      ! `nz·H_VANISHED`), and the checkpoint saves that state verbatim.
      ! Re-seeding after a restart read rewrote every land column back to
      ! uniform `H_VANISHED` -- column total unchanged, layout not -- so the
      ! resumed run was not the run that wrote the file (1/4-degree Southern
      ! Ocean, 2026-10-01: 56 046 land columns x 50 layers per rank).
      seed_land = .true.
      if (present(warm_restart)) seed_land = .not. warm_restart
      if (seed_land) call ocean_state_seed_land_cells(ocean_state, grid)

      if (compute_rank == 0) then
         associate (unused => cfg%ocean%grid%grid_config)
         end associate
      end if
   end subroutine configure_ocean_land_mask

   pure function metrics_dx_min(metrics, grid) result(dx_min)
      !! Representative minimum grid length over the PHYSICAL region,
      !! taken over both `dxT` and `dyT` (host-side, configure time).
      !! Bit-identical to `min(grid%dx, grid%dy)` on uniform Cartesian.
      type(ocean_metrics_t), intent(in) :: metrics
      type(hgrid_t), intent(in) :: grid
      real(wp) :: dx_min
      integer :: ng, i0, i1, j0, j1
      ng = grid%nghost
      i0 = ng + 1
      i1 = ng + grid%nx_phys
      j0 = ng + 1
      j1 = ng + grid%ny_phys
      dx_min = min(minval(metrics%dxT(i0:i1, j0:j1)), &
                   minval(metrics%dyT(i0:i1, j0:j1)))
   end function metrics_dx_min

   pure function metrics_bt_cfl_length(metrics, grid) result(l_cfl)
      !! 2-D external-gravity-wave CFL length over the PHYSICAL region:
      !!     l_cfl = min_cell  1 / sqrt(1/dxT^2 + 1/dyT^2).
      !! Length scale for the barotropic CFL `c_ext*dt*sqrt(1/dx^2+1/dy^2) <= 1`
      !! (includes the cross-direction term; on uniform Cartesian = dx/sqrt(2)).
      !! Handles anisotropic cells exactly.  Host-side, configure time.
      type(ocean_metrics_t), intent(in) :: metrics
      type(hgrid_t), intent(in) :: grid
      real(wp) :: l_cfl, inv_l2_max
      integer :: ng, i0, i1, j0, j1
      ng = grid%nghost
      i0 = ng + 1
      i1 = ng + grid%nx_phys
      j0 = ng + 1
      j1 = ng + grid%ny_phys
      inv_l2_max = maxval(1.0_wp/metrics%dxT(i0:i1, j0:j1)**2 &
                          + 1.0_wp/metrics%dyT(i0:i1, j0:j1)**2)
      l_cfl = 1.0_wp/sqrt(inv_l2_max)
   end function metrics_bt_cfl_length

   pure function bt_auto_n_inner(dt_outer, cfl_safety, c_ext, l_cfl) result(n_inner)
      !! Smallest barotropic substep count `n_inner` such that the substep
      !! `dt_outer/n_inner` satisfies the 2-D external-gravity-wave CFL:
      !!     dt_bt <= cfl_safety * l_cfl / c_ext,
      !! with `l_cfl` the 2-D CFL length (`metrics_bt_cfl_length`) and `c_ext`
      !! the external wave speed `sqrt(g*H_max)`.  MOM6 set_dtbt analogue, now
      !! with the cross-direction term included (the legacy estimate used a
      !! 1-D length and under-counted n_inner by ~sqrt(2) on square cells,
      !! leaving the effective 2-D CFL at ~0.92 for cfl_bt_safety=0.65 — on the
      !! edge of the forward-backward scheme's stability).
      real(wp), intent(in) :: dt_outer, cfl_safety, c_ext, l_cfl
      integer :: n_inner
      real(wp) :: dt_bt_safe
      dt_bt_safe = cfl_safety*l_cfl/c_ext
      n_inner = bt_auto_n_inner_from_dt(dt_outer, dt_bt_safe)
   end function bt_auto_n_inner

   pure function bt_auto_n_inner_from_dt(dt_outer, dt_bt_safe) result(n_inner)
      !! Smallest `n_inner >= 1` with `dt_outer/n_inner <= dt_bt_safe`,
      !! for an ALREADY-LIMITED safe barotropic substep `dt_bt_safe` (s) —
      !! the per-wet-cell minimum `bt_cfl_dt_wet` returns, reduced across
      !! ranks.  `bt_auto_n_inner` is this with `dt_bt_safe` formed from a
      !! single `(c_ext, l_cfl)` pair.
      real(wp), intent(in) :: dt_outer
         !! Outer (baroclinic) step (s).
      real(wp), intent(in) :: dt_bt_safe
         !! Largest stable barotropic substep, safety factor included (s).
      integer :: n_inner
      n_inner = max(1, ceiling(dt_outer/dt_bt_safe))
   end function bt_auto_n_inner_from_dt

   pure subroutine bt_cfl_dt_wet(nx, ny, i0, i1, j0, j1, b, wet, dxT, dyT, &
                                 cfl_safety, dt_bt, h_at, l_at, n_wet)
      !! Per-WET-CELL external-gravity-wave CFL limit (MOM6 `set_dtbt`):
      !!
      !!     dt_bt = min over wet (i,j) of  cfl_safety * l(i,j) / c(i,j),
      !!     l(i,j) = 1/sqrt(1/dxT(i,j)^2 + 1/dyT(i,j)^2),
      !!     c(i,j) = sqrt(g * max(b(i,j), 1 m)),
      !!
      !! i.e. the LOCAL depth with the LOCAL cell size, over OCEAN only.
      !! MOM6 evaluates the same quantity as `gtot*dt^2*(1/dx^2+1/dy^2)`
      !! per wet point.
      !!
      !! **Why per point.** The former estimate combined the deepest depth
      !! ANYWHERE with the smallest cell ANYWHERE — on the 1° tripolar grid
      !! that was 6000 m of ocean against a 362 m LAND cell at a
      !! land-locked bipole, and bought 1930 substeps where ~32 suffice.
      !! A land cell carries no gravity wave, and a small cell over a
      !! shallow shelf does not see the abyssal wave speed.
      !!
      !! **Bit-identity where the two agree.** Each point's value is
      !! evaluated with EXACTLY the arithmetic the global-extremes estimate
      !! used (`1/sqrt(inv_l2)`, `sqrt(g*max(b,1))`, `safety*l/c`, in that
      !! order).  Every step is monotone under round-to-nearest, so where
      !! the deepest wet column and the smallest wet cell COINCIDE (a
      !! flat-bottomed uniform grid, a wall basin with no land) the minimum
      !! is attained at that point and equals the old number bit-for-bit.
      !!
      !! No wet cell in the window ⇒ `dt_bt = huge`, `n_wet = 0` — the
      !! identity of the cross-rank `min` reduction (a rank that is all
      !! land does not constrain the others).
      !!
      !! Host-side, configure time: plain loops, no `do concurrent`.
      integer, intent(in) :: nx
         !! First extent of the centre arrays (ghosts included).
      integer, intent(in) :: ny
         !! Second extent of the centre arrays (ghosts included).
      integer, intent(in) :: i0
         !! First physical i.
      integer, intent(in) :: i1
         !! Last physical i.
      integer, intent(in) :: j0
         !! First physical j.
      integer, intent(in) :: j1
         !! Last physical j.
      real(wp), intent(in) :: b(nx, ny)
         !! Bed depth (m, positive down).
      real(wp), intent(in) :: wet(nx, ny)
         !! Static wet (1) / land (0) T-cell mask.
      real(wp), intent(in) :: dxT(nx, ny)
         !! T-cell x length (m).
      real(wp), intent(in) :: dyT(nx, ny)
         !! T-cell y length (m).
      real(wp), intent(in) :: cfl_safety
         !! `&ocean_bt_nml cfl_bt_safety`.
      real(wp), intent(out) :: dt_bt
         !! Smallest per-wet-cell safe substep (s); `huge` if no wet cell.
      real(wp), intent(out) :: h_at
         !! Bed depth at the limiting cell (m); 0 if no wet cell.
      real(wp), intent(out) :: l_at
         !! 2-D CFL length at the limiting cell (m); 0 if no wet cell.
      integer, intent(out) :: n_wet
         !! Wet cells scanned.
      integer :: i, j
      real(wp) :: inv_l2, l_ij, c_ij, dt_ij

      dt_bt = huge(1.0_wp)
      h_at = 0.0_wp
      l_at = 0.0_wp
      n_wet = 0
      do j = j0, j1
         do i = i0, i1
            if (wet(i, j) <= 0.5_wp) cycle
            n_wet = n_wet + 1
            inv_l2 = 1.0_wp/dxT(i, j)**2 + 1.0_wp/dyT(i, j)**2
            l_ij = 1.0_wp/sqrt(inv_l2)
            c_ij = sqrt(GRAVITY*max(b(i, j), 1.0_wp))
            dt_ij = cfl_safety*l_ij/c_ij
            if (dt_ij < dt_bt) then
               dt_bt = dt_ij
               h_at = b(i, j)
               l_at = l_ij
            end if
         end do
      end do
   end subroutine bt_cfl_dt_wet

   subroutine fill_coriolis_corner(cfg, metrics, grid, f_corner)
      !! Fill a C-grid corner Coriolis array via the single
      !! generator-driven routine `metrics_fill_coriolis`, honouring
      !! `&ocean_grid_nml coriolis_scheme` (D7).  beta_plane (default)
      !! is BIT-IDENTICAL to the legacy `coriolis_adv_set_beta_plane`;
      !! planetary uses the metrics geography.  The centre output is
      !! discarded here (filled into local scratch).
      type(config_t), intent(in) :: cfg
      type(ocean_metrics_t), intent(in) :: metrics
      type(hgrid_t), intent(in) :: grid
      real(wp), intent(out) :: f_corner(:, :)
      real(wp), allocatable :: f_centre_scratch(:, :)
      allocate (f_centre_scratch(grid%nx_total, grid%ny_total))
      call metrics_fill_coriolis(metrics, &
                                 parse_coriolis_scheme(cfg%ocean%grid%coriolis_scheme), &
                                 cfg%coriolis_f, cfg%ocean%topo%coriolis_beta, &
                                 cfg%ocean%topo%coriolis_y_ref, cfg%ocean%grid%omega, &
                                 grid, f_corner, f_centre_scratch)
      deallocate (f_centre_scratch)
   end subroutine fill_coriolis_corner

   subroutine fill_coriolis_centre(cfg, metrics, grid, f_centre)
      !! Fill a cell-centre |Coriolis| array via `metrics_fill_coriolis`
      !! (D7).  beta_plane (default) is BIT-IDENTICAL to the legacy
      !! EPBL / kappa-shear `set_f_centre`.  The corner output is
      !! discarded here (filled into local scratch).
      type(config_t), intent(in) :: cfg
      type(ocean_metrics_t), intent(in) :: metrics
      type(hgrid_t), intent(in) :: grid
      real(wp), intent(out) :: f_centre(:, :)
      real(wp), allocatable :: f_corner_scratch(:, :)
      allocate (f_corner_scratch(grid%nx_total + 1, grid%ny_total + 1))
      call metrics_fill_coriolis(metrics, &
                                 parse_coriolis_scheme(cfg%ocean%grid%coriolis_scheme), &
                                 cfg%coriolis_f, cfg%ocean%topo%coriolis_beta, &
                                 cfg%ocean%topo%coriolis_y_ref, cfg%ocean%grid%omega, &
                                 grid, f_corner_scratch, f_centre)
      deallocate (f_corner_scratch)
   end subroutine fill_coriolis_centre

   subroutine configure_ocean_forcing(cfg, ocean_state, grid, compute_rank, decomp)
      !! Wire surface wind stress, horizontal-viscosity coefficients, and the
      !! Coriolis beta-plane + PV-scheme variant from cfg into the ocean slots.
      !!
      !! When `decomp` is present the wind-stress setters receive the rank's
      !! global meridional offset and global domain extent so each subdomain
      !! seeds the correct portion of the global wind profile.  Absent ⇒
      !! single-rank byte-identical path.
      type(config_t), intent(in) :: cfg
      type(ocean_state_t), intent(inout) :: ocean_state
      type(hgrid_t), intent(in) :: grid
      integer, intent(in) :: compute_rank
      type(decomp_t), intent(in), optional :: decomp

      integer :: joff, nyg

      joff = 0
      nyg = grid%ny_phys
      if (present(decomp)) then
         joff = decomp%j_start - 1
         nyg = decomp%ny_global
      end if

      ! Wind stress: dispatch on cfg%ocean%topo%wind_config.
      select case (trim(cfg%ocean%topo%wind_config))
      case ("2gyre")
         call ocean_state%surface_stress%set_wind_stress_2gyre( &
            grid, cfg%ocean%topo%taux_magnitude, &
            j_offset=joff, ny_global=nyg)
      case ("neverworld2")
         call ocean_state%surface_stress%set_wind_stress_neverworld2( &
            grid, cfg%ocean%topo%taux_magnitude, &
            j_offset=joff, ny_global=nyg)
      case default  ! "constant"
         ! Warn loudly when a wind was configured on the knob this path does
         ! NOT read. `taux_magnitude` is only consumed by the "2gyre" and
         ! "neverworld2" profiles above; the constant path reads
         ! `&physics_nml wind_stress_x/y`. Setting the former under
         ! `wind_config="constant"` is silently inert — which is exactly how
         ! `coriolis_coast.nml` shipped a case whose header promises a
         ! "wind-spun-up gyre" that it has never generated: it ran its full
         ! 10 days at En = 0.000E+00.
         if (abs(cfg%ocean%topo%taux_magnitude) > 0.0_wp .and. &
             abs(cfg%wind_stress_x) <= 0.0_wp .and. &
             abs(cfg%wind_stress_y) <= 0.0_wp) then
            call logger%warning( &
               "&ocean_topo_nml taux_magnitude = "// &
               to_string(cfg%ocean%topo%taux_magnitude)//" is INERT under "// &
               'wind_config="constant" (it is only read by "2gyre" / '// &
               '"neverworld2"), and &physics_nml wind_stress_x/y are both '// &
               "zero, so this run has NO wind forcing at all. Set "// &
               "&physics_nml wind_stress_x to apply a constant wind.")
         end if
         call ocean_state%surface_stress%set_wind_stress_const( &
            cfg%wind_stress_x, cfg%wind_stress_y)
      end select

      ! Make the freshly-seeded wind valid in the ghost bands, then derive
      ! stress_mag from it.  Host-side (device_resident=.false.): this runs
      ! in the driver's configure phase, ahead of ocean_state_enter_data.
      !
      ! The exchange matters even though the wind is configure-static.
      ! `set_wind_stress_2gyre` / `_neverworld2` fill PHYSICAL rows only and
      ! leave ghost rows at zero ("so wall faces see no spurious stress"),
      ! which is right at a true wall and wrong at an MPI seam — there the
      ! ghost belongs to the neighbour rank and must carry its wind, not 0.
      ! Nothing else ever repairs it, and both `stress_mag` (KPP/EPBL u_*)
      ! and the MLE corner average read one cell beyond their own.  At a
      ! true wall the exchange is a no-op, so the zero-ghost intent of the
      ! analytic setters is preserved exactly (and `test_wind_2gyre_ghosts`
      ! still holds).
      call ocean_seam_refresh_surface_stress(ocean_state%surface_stress, grid, &
                                             ocean_state%bc, device_resident=.false.)

      ! Horizontal momentum viscosity (default 0 keeps analytical tests bit-
      ! identical; realistic wind-driven runs need a non-zero value).
      ocean_state%hvisc%nu_h = cfg%ocean%hvisc%nu_h
      ocean_state%hvisc%nu_4 = cfg%ocean%hvisc%nu_4
      ocean_state%hvisc%stress_tensor = cfg%ocean%hvisc%stress_tensor
      ocean_state%hvisc%bound_coef = cfg%ocean%hvisc%bound_coef
      ocean_state%hvisc%bound_kh = cfg%ocean%hvisc%bound_kh
      ocean_state%hvisc%no_slip = cfg%ocean%hvisc%no_slip
      ! Fail-loud guard (2026-07-28 dt=800 forensics): bound_kh clamps the
      ! effective per-cell viscosity to
      ! kh_max = bound_coef·0.125/(dt·(1/dx² + 1/dy²)), so a configured
      ! nu_h far above kh_max is silently unreachable and the run is
      ! under-damped relative to its nml intent — the dt=800 blow-up
      ! family was exactly this (bound_coef=0.15 capped the effective nu
      ! at ~177 m²/s against nu_h=10000).  Warning, not error: flow-aware
      ! closures legitimately over-provision constant floors/ceilings.
      ! CARTESIAN only: `grid%dx`/`grid%dy` are the cell size there, but a
      ! placeholder on a curvilinear grid (1 m on a supergrid, degrees on a
      ! spherical sector) — which made the estimate ~1e-5 m2/s and the
      ! warning fire, falsely, on every global run.  Curvilinear grids are
      ! covered per wet cell by the viscous-CFL stability audit.
      if (cfg%ocean%hvisc%bound_kh .and. cfg%dt_fixed > 0.0_wp &
          .and. parse_grid_config(cfg%ocean%grid%grid_config) == GRID_CONFIG_CARTESIAN &
          .and. grid%dx > 0.0_wp .and. grid%dy > 0.0_wp) then
         block
            real(wp) :: kh_max_est
            kh_max_est = cfg%ocean%hvisc%bound_coef*0.125_wp/ &
                         (cfg%dt_fixed*(1.0_wp/grid%dx**2 + 1.0_wp/grid%dy**2))
            if (cfg%ocean%hvisc%nu_h > 10.0_wp*kh_max_est .and. compute_rank == 0) then
               call logger%warning("hvisc: nu_h = "//to_string(cfg%ocean%hvisc%nu_h)// &
                                   " m2/s is unreachable — bound_kh clamps the effective "// &
                                   "viscosity to ~"//to_string(kh_max_est)// &
                                   " m2/s at this dx/dt (bound_coef = "// &
                                   to_string(cfg%ocean%hvisc%bound_coef)// &
                                   ").  The run will be under-damped relative to the nml "// &
                                   "intent; raise bound_coef (MOM6 default 0.8), reduce dt, "// &
                                   "or lower nu_h to what the clamp admits.")
            end if
         end block
      end if
      ! Anisotropic viscosity (Smith & McWilliams 2003) — stress-tensor
      ! path only.  Default kh_aniso=0 ⇒ isotropic, bit-identical.  The
      ! constant direction tensor is precomputed once from aniso_dir
      ! (mode 0 = grid-relative).  `aniso_mode /= 0` is rejected at
      ! configure (validate_config); the else below is a defensive guard.
      ocean_state%hvisc%kh_aniso = cfg%ocean%hvisc%kh_aniso
      if (cfg%ocean%hvisc%aniso_mode == 0) then
         call ocean_hvisc_set_aniso_direction(ocean_state%hvisc, &
                                              cfg%ocean%hvisc%aniso_dir(1), &
                                              cfg%ocean%hvisc%aniso_dir(2))
      else
         call ocean_hvisc_set_aniso_direction(ocean_state%hvisc, 1.0_wp, 0.0_wp)
      end if

      ! Coriolis: beta = 0 → uniform f_0 = cfg%coriolis_f; non-zero beta engages
      ! f(y) = f_0 + beta*(y - y_ref).  Filled by the single generator-driven
      ! routine (D7) — beta_plane is bit-identical to the legacy setter; the
      ! `f_0`/`beta` diagnostics are recorded here (the generator only fills
      ! the array).
      ocean_state%coriolis_adv%f_0 = cfg%coriolis_f
      ocean_state%coriolis_adv%beta = cfg%ocean%topo%coriolis_beta
      call fill_coriolis_corner(cfg, ocean_state%metrics, grid, &
                                ocean_state%coriolis_adv%f_corner)
      ! Tripolar: fold the STATIC f_corner once at configure (it is read
      ! straight from f_corner in the BT substep + Coriolis-adv, never
      ! re-wrapped per step).  f is a SCALAR under the fold — the reflected
      ! corner sits at the SAME latitude, so it COPIES (negate=.false.), no
      ! sign flip.  Periodic-x of the corner columns first (Appendix A
      ! ordering), then the north fold.  No-op for non-tripolar runs.
      if (trim(cfg%ocean%bc%north) == "tripolar_fold") then
         call fill_f_corner_seam_ghosts(grid, ocean_state%coriolis_adv%f_corner)
      end if
      ocean_state%coriolis_adv%pv_variant = parse_pv_variant(cfg%ocean%coriolis%form)
      ocean_state%coriolis_adv%use_hk_correction = &
         (ocean_state%coriolis_adv%pv_variant == PV_VARIANT_SADOURNY_HK)
      ocean_state%coriolis_adv%pv_adv_scheme = &
         parse_pv_adv_scheme(cfg%ocean%coriolis%pv_adv_scheme)
      ! HK pair floor on the coordinates with STATIC bed fillers, closed
      ! faces or not (see `coriolis_adv_t%hk_pair_floor`).
      ocean_state%coriolis_adv%hk_pair_floor = &
         any(parse_ocean_vcoord_type(cfg%vcoord_type) == &
             [VCOORD_Z_FIXED, VCOORD_ZSTAR, VCOORD_ZSTAR_FULL])
      ! Coastal lateral BC (land mask, C1): free-slip default, shared knob.
      ocean_state%coriolis_adv%no_slip = cfg%ocean%hvisc%no_slip
      ! Mass-consistent CorAdCalc (MOM6 parity); config fail-loud
      ! guarantees form="sadourny_energy" + split_scheme="pred_corr".
      ocean_state%coriolis_adv%state_fluxes = cfg%ocean%coriolis%use_state_fluxes
      ! BOUND_CORIOLIS velocity-form clamp (energy scheme only; config
      ! fail-loud guarantees form="sadourny_energy").
      ocean_state%coriolis_adv%bound_coriolis = cfg%ocean%coriolis%bound_coriolis
      ! PV corner-thickness construction (energy scheme only, config fail-loud).
      if (trim(cfg%ocean%coriolis%corner_h) == "mom6_area") then
         ocean_state%coriolis_adv%corner_h_variant = CORNER_H_MOM6_AREA
      else
         ocean_state%coriolis_adv%corner_h_variant = CORNER_H_CELL_MEAN
      end if
      if (compute_rank == 0) then
         call logger%info("Coriolis form:    "//trim(cfg%ocean%coriolis%form))
         if (cfg%ocean%coriolis%use_state_fluxes) then
            call logger%info("Coriolis CorAdv:  mass-consistent state fluxes "// &
                             "in the mom6 corrector (MOM6 uh/vh parity)")
         end if
         if (cfg%ocean%coriolis%bound_coriolis) then
            call logger%info("Coriolis bound:   BOUND_CORIOLIS ON — energy-scheme accel "// &
                             "clamped to the (f+zeta)*v velocity-form range (MOM6)")
         end if
         if (trim(cfg%ocean%coriolis%corner_h) == "mom6_area") then
            call logger%info("Coriolis corner:  PV corner-h = mom6_area (MOM6 "// &
                             "Area_q/(hArea_q+vol_neglect); round-off-identical to "// &
                             "cell_mean above H_MIN_PV)")
         end if
      end if
   end subroutine configure_ocean_forcing

   subroutine fill_f_corner_seam_ghosts(grid, f_corner)
      !! Periodic-x wrap + north-fold (scalar copy) of the static corner
      !! Coriolis array for a tripolar grid.  f is reflection-invariant
      !! (same latitude at the conjugate corner), so negate=.false.
      !!
      !! Decomposed: only an axis this tile holds WHOLE is wrapped, and only
      !! the tile that owns the north edge folds (the `seed_wrap_static_2d`
      !! rule).  Every other seam ghost was already filled by the generator
      !! from the global-slice geography (`metrics_fill_tripolar`), which is
      !! exactly the neighbour's corner value.
      type(hgrid_t), intent(in) :: grid
      real(wp), intent(inout) :: f_corner(:, :)
      integer :: ng, ni, i, j
      ng = grid%nghost
      ni = grid%nx_phys
      ! px > 1: x ghosts come from the generator; the fold runs later, in
      ! engine_setup, through the distributed exchange (needs its plan).
      if (grid%nx_phys /= grid%nx_global) return
      ! Periodic-x ghost columns (corner/Bu face-type: physical i=ng+1..ng+ni+1).
      do j = 1, size(f_corner, 2)
         do i = 1, ng
            f_corner(i, j) = f_corner(i + ni, j)
            f_corner(ng + ni + 1 + i, j) = f_corner(ng + 1 + i, j)
         end do
      end do
      ! North fold (copy) — the north-edge tile only.
      if (grid%j_offset_global + grid%ny_phys /= grid%ny_global) return
      call fold_north_corner(f_corner, grid%nx_total + 1, grid%ny_total + 1, &
                             grid%nx_phys, grid%ny_phys, grid%nghost, negate=.false.)
   end subroutine fill_f_corner_seam_ghosts

   subroutine configure_ocean_drag(cfg, ocean_state, compute_rank, ierr)
      !! Bottom-drag variant + coefficients, the continuity PPM positivity
      !! guard, and the BT-budget diagnostic probe — plus their rank-0 log
      !! lines.  All knobs default off/zero (bit-identical to pre-knob nmls).
      type(config_t), intent(in) :: cfg
      type(ocean_state_t), intent(inout) :: ocean_state
      integer, intent(in) :: compute_rank
      integer, intent(out), optional :: ierr
         !! Non-zero on an isopycnal-floor configuration conflict when
         !! present; absent behaves as today (`error stop`).

      ! EOS variant FIRST: this is the earliest configure_ocean_* call, so
      ! `ocean_state%eos%variant` is set before the EOS-handle is copied into
      ! the vmix / EPBL / kappa-shear slots (in configure_ocean_lateral /
      ! _epbl / _kappa_shear).  The device-callable gate + the FV_WRIGHT
      ! cross-check run later, in configure_ocean_pgf (post pgf-variant parse).
      ocean_state%eos%variant = parse_eos_variant(cfg%ocean%eos%eos)
      ! Potential-density reference pressure.  Assigned HERE, in the
      ! earliest configure_ocean_* call, so the value is already on the
      ! handle when it is copied into the vmix / EPBL / kappa-shear slots
      ! further down — and before `enter_data`, so the mapped copies carry
      ! it.  `eos_t` is a flat POD read by value into the `_impl` calls, so
      ! this host assignment owes no `!$acc update device` under
      ! mem:separate (same contract as `rho0`/`rho_ref` on the PGF slot).
      ! Horizontally uniform by design — see the knob's FORD docstring.
      ocean_state%eos%p_ref = cfg%ocean%eos%p_ref
      ! Freezing-point (liquidus) coefficient set — same site, same
      ! reasons, and one more: the ICE slot reads the god-state handle
      ! directly (`engine%state%eos` is what `ice_frazil_accumulate`,
      ! `ice_frazil_uptake` and `ice_compute_basal_flux` are handed), so
      ! landing the set here puts it on the ONE handle every liquidus
      ! consumer sees, before any copy is taken and before `enter_data`.
      ! An unrecognised string is already fatal in `validate_config`;
      ! `eos_apply_tfreeze_set` leaves the handle untouched for it.
      call eos_apply_tfreeze_set(ocean_state%eos, &
                                 parse_tfreeze_set(cfg%ocean%eos%tfreeze_set))
      if (compute_rank == 0) then
         call logger%info("EOS variant:      "//trim(cfg%ocean%eos%eos))
         if (parse_tfreeze_set(cfg%ocean%eos%tfreeze_set) /= TFREEZE_SET_SEAICE) then
            call logger%info("Liquidus set:     "//trim(cfg%ocean%eos%tfreeze_set)// &
                             " (ISOMIP+ / Asay-Davis et al. 2016 Table 4; the "// &
                             "default 'seaice' SIS2 set is ~0.03 degC warmer at S=34.5)")
         end if
      end if

      ocean_state%bdrag%variant = parse_bdrag_variant(cfg%ocean%bdrag%form)
      ocean_state%bdrag%c_drag = cfg%ocean%bdrag%cd
      ocean_state%bdrag%r_linear = cfg%ocean%bdrag%r
      ocean_state%bdrag%hbbl = cfg%ocean%bdrag%hbbl
      ocean_state%bdrag%drag_bg_vel = cfg%ocean%bdrag%bg_vel
      ocean_state%bdrag%bbl_thick_min = cfg%ocean%bdrag%bbl_thick_min
      ocean_state%bdrag%bed_factor = cfg%ocean%bdrag%bed_factor
      ocean_state%bdrag%channel_drag = cfg%ocean%bdrag%channel_drag
      ocean_state%bdrag%cdrag_side = cfg%ocean%bdrag%cdrag_side
      ocean_state%bdrag%implicit = cfg%ocean%bdrag%implicit

      ! Continuity PPM positivity guard (MOM6 PPM_limit_pos analogue).
      ocean_state%continuity%use_ppm_limit_pos = cfg%ocean%continuity%ppm_limit_pos
      ocean_state%continuity%renorm_consistent_flux = cfg%ocean%continuity%renorm_consistent_flux
      ! Wet/dry composition: the Newton uhbt renormalisation's donor
      ! re-pick + CFL bracket destabilise the drying front (h_layer goes
      ! negative, test_ocean_wetdry_driver) — wet/dry keeps the legacy
      ! single-step form it was validated on.
      ocean_state%continuity%renorm_legacy_single_step = cfg%ocean%wetdry%enable
      ocean_state%continuity%h_min = cfg%ocean%continuity%h_min
      ocean_state%continuity%vol_cfl = cfg%ocean%continuity%vol_cfl
      if (compute_rank == 0 .and. cfg%ocean%continuity%ppm_limit_pos) then
         call logger%info("Continuity PPM:   positivity guard ON (MOM6 PPM_limit_pos), "// &
                          "h_min = "//to_string(cfg%ocean%continuity%h_min)//" m")
      end if
      ! Positive-definite split continuity (MOM6-prevention + Roundabout-conservation).
      ! h_lim = angstrom_h on VCOORD_LAGRANGIAN, else 0 (⇒ P1 floor inert on
      ! non-Lagrangian coords).  Default off ⇒ bit-identical.
      ocean_state%continuity%positive_definite = cfg%ocean%continuity%positive_definite
      if (parse_ocean_vcoord_type(cfg%vcoord_type) == VCOORD_LAGRANGIAN) then
         ocean_state%continuity%h_lim = cfg%ocean%isopycnal%angstrom_h
      else
         ocean_state%continuity%h_lim = 0.0_wp
      end if
      if (compute_rank == 0 .and. cfg%ocean%continuity%positive_definite) then
         call logger%info("Continuity PD:    positive_definite ON (h>=h_lim, zero mass "// &
                          "created), h_lim = "//to_string(ocean_state%continuity%h_lim)//" m")
      end if
      ! Phase-1 Lagrangian minimum-thickness floor (MOM6 Angstrom_H analogue).
      ! Only passed to the h-update kernels when the active vcoord is
      ! VCOORD_LAGRANGIAN (gated in rdb_ocean_dyn at the call site). 0 = off.
      ocean_state%continuity%angstrom_h = cfg%ocean%isopycnal%angstrom_h
      if (compute_rank == 0 .and. cfg%ocean%isopycnal%angstrom_h > 0.0_wp) then
         call logger%info("Isopycnal floor:  angstrom_h = "// &
                          to_string(cfg%ocean%isopycnal%angstrom_h)//" m (Lagrangian only)")
      end if
      ! Conservative minimum-thickness mode: replaces the injecting floor with a
      ! per-column borrow (rdb_ocean_min_thickness), invoked from the dyn
      ! continuity site. Fail-loud: needs angstrom_h > 0 AND VCOORD_LAGRANGIAN.
      ocean_state%continuity%conservative_floor = cfg%ocean%isopycnal%conservative_floor
      if (cfg%ocean%isopycnal%conservative_floor) then
         if (cfg%ocean%isopycnal%angstrom_h <= 0.0_wp) then
            call fail("ocean_isopycnal_nml: conservative_floor=.true. requires "// &
                      "angstrom_h > 0 (nothing to floor to).", ierr, OCEAN_STATUS_ERR_SETUP)
            return
         end if
         if (parse_ocean_vcoord_type(cfg%vcoord_type) /= VCOORD_LAGRANGIAN) then
            call fail("ocean_isopycnal_nml: conservative_floor=.true. is only "// &
                      "valid with vcoord_type='lagrangian' (isopycnal path).", ierr, OCEAN_STATUS_ERR_SETUP)
            return
         end if
         if (compute_rank == 0) then
            call logger%info("Isopycnal floor:  conservative_floor = ON "// &
                             "(per-column borrow, mass/momentum/tracer conserving)")
         end if
      end if
      ! Phase-2/3 Lagrangian grounding-stability knobs on ocean_dyn_t.
      ! Gates tested at the dyn call sites; bit-identical when off.
      ocean_state%dyn%angstrom_h = cfg%ocean%isopycnal%angstrom_h
      ocean_state%dyn%reset_vanished_u = cfg%ocean%isopycnal%reset_vanished_u
      ocean_state%dyn%cfl_ignore_vanished = cfg%ocean%isopycnal%cfl_ignore_vanished
      ocean_state%dyn%check_h_positive = cfg%ocean%isopycnal%check_h_positive
      if (compute_rank == 0) then
         if (cfg%ocean%isopycnal%reset_vanished_u) then
            call logger%info("Isopycnal reset:  reset_vanished_u = ON (Lagrangian only)")
         end if
         if (cfg%ocean%isopycnal%cfl_ignore_vanished) then
            call logger%info("Isopycnal CFL:    cfl_ignore_vanished = ON (Lagrangian only)")
         end if
      end if
      if (compute_rank == 0 .and. cfg%ocean%continuity%vol_cfl) then
         call logger%info("Continuity PPM:   swept-volume face thickness ON (MOM6 vol_CFL)")
      end if

      ! Ghost-band poison debug knob.
      ocean_state%dyn%poison_ghosts = cfg%ocean%mpi%poison_ghosts

      ! BT-budget diagnostic probe — heavy when on (D->H + 16 prints/step).
      ocean_state%dyn%debug_bt_budget = cfg%ocean%debug%budget
      if (compute_rank == 0 .and. cfg%ocean%debug%budget) then
         call logger%info("BT-budget probe:  ENABLED (per-step per-region "// &
                          "power decomposition)")
      end if

      ! KE attribution meter — heavy when on (serialises the apply chain).
      ocean_state%dyn%ke_probe%enable = cfg%ocean%debug%ke_attr
      ocean_state%dyn%ke_probe%start_step = cfg%ocean%debug%ke_attr_start_step
      ocean_state%dyn%ke_probe%end_step = cfg%ocean%debug%ke_attr_end_step
      ocean_state%dyn%chksum_probe%enable = cfg%ocean%debug%chksum
      ocean_state%dyn%chksum_probe%start_step = cfg%ocean%debug%chksum_start_step
      ocean_state%dyn%chksum_probe%end_step = cfg%ocean%debug%chksum_end_step
      ocean_state%dyn%chksum_probe%interior = cfg%ocean%debug%chksum_interior
      if (compute_rank == 0 .and. cfg%ocean%debug%ke_attr) then
         call logger%info("KE-attr meter:    ENABLED (per-segment layer-KE "// &
                          "rows, steps "//to_string(cfg%ocean%debug%ke_attr_start_step)// &
                          " to "//to_string(cfg%ocean%debug%ke_attr_end_step)//")")
      end if
      if (compute_rank == 0) then
         call logger%info("Bottom drag:      "//trim(cfg%ocean%bdrag%form)// &
                          "  C_d="//to_string(cfg%ocean%bdrag%cd)// &
                          "  r="//to_string(cfg%ocean%bdrag%r)//" 1/s")
         if (cfg%ocean%bdrag%hbbl > 0.0_wp) then
            call logger%info("Bottom drag BBL:  HBBL="//to_string(cfg%ocean%bdrag%hbbl)// &
                             " m  bg_vel="//to_string(cfg%ocean%bdrag%bg_vel)// &
                             " m/s  thick_min="//to_string(cfg%ocean%bdrag%bbl_thick_min)//" m")
         else
            call logger%info("Bottom drag BBL:  bed-layer only (HBBL=0)")
            ! Bed-only mode drags the face's first LIVE layer `k_bot_u/v`.
            ! Under z_fixed that index is filled at configure
            ! (`configure_ocean_k_bot`) and skips the static bed fillers, so
            ! the bed-only mode reaches water there.  `k_bot ≡ 1` under
            ! zstar_full (its bed-side layers vanish DYNAMICALLY, no static
            ! pattern) and, until `configure_ocean_k_bot` learns it, under
            ! zstar (static z_fixed fillers): k = 1 is an inert filler in
            ! every column shallower than the deepest nominal interface and
            ! almost the whole domain runs with NO bottom drag.  HBBL mode
            ! accumulates thickness from the bed up, skips those fillers and
            ! reaches the live bottom layer.
            if ((cfg%ocean%bdrag%cd > 0.0_wp .or. cfg%ocean%bdrag%r > 0.0_wp) .and. &
                (parse_ocean_vcoord_type(cfg%vcoord_type) == VCOORD_ZSTAR .or. &
                 parse_ocean_vcoord_type(cfg%vcoord_type) == VCOORD_ZSTAR_FULL)) then
               call logger%warning("&ocean_bdrag_nml hbbl = 0 (bed-layer-only drag) under "// &
                                   "vcoord_type = '"//trim(cfg%vcoord_type)//"': the drag "// &
                                   "acts on layer k = 1, an inert filler in every column "// &
                                   "shallower than the deepest nominal layer, so those "// &
                                   "columns get NO bottom drag.  Set hbbl > 0 (MOM6 "// &
                                   "OM4/OM_1deg: HBBL = 10 m, bg_vel = 0.1 m/s).")
            end if
         end if
         if (cfg%ocean%bdrag%channel_drag) then
            call logger%info("Channel drag:     ON  cdrag_side="// &
                             to_string(cfg%ocean%bdrag%cdrag_side))
         end if
      end if
      if (present(ierr)) ierr = OCEAN_STATUS_OK
   end subroutine configure_ocean_drag

   subroutine configure_ocean_hdiff(cfg, ocean_state, compute_rank)
      !! Along-coordinate tracer Laplacian coefficient (`&ocean_hdiff_nml
      !! kappa_h`).  Scalar copy onto `ocean_state%hdiff_tracer`, mapped
      !! with its parent slot at `ocean_state_enter_data` — no explicit
      !! `!$acc update` needed as long as this runs before that (it does;
      !! see `rdb_driver.F90`).  Default `kappa_h = 0.0` leaves the
      !! kernel's short-circuit intact ⇒ bit-identical.
      type(config_t), intent(in) :: cfg
      type(ocean_state_t), intent(inout) :: ocean_state
      integer, intent(in) :: compute_rank

      ocean_state%hdiff_tracer%kappa_h = cfg%ocean%hdiff%kappa_h
      if (compute_rank == 0 .and. cfg%ocean%hdiff%kappa_h > 0.0_wp) then
         call logger%info("Tracer hdiff:     along-coordinate kappa_h = "// &
                          to_string(cfg%ocean%hdiff%kappa_h)//" m^2/s")
      end if
   end subroutine configure_ocean_hdiff

   subroutine configure_ocean_vmix(cfg, ocean_state, compute_rank, ierr)
      !! Thermodynamics on/off, velocity-truncation clamp (MAXVEL), DIRECT_STRESS
      !! surface-stress distribution, KV_ML_INVZ2 surface-band viscosity,
      !! HARMONIC_VISC face-thickness mean, and the DT_THERM thermo/tracer
      !! cadence — with their rank-0 log lines.
      type(config_t), intent(in) :: cfg
      type(ocean_state_t), intent(inout) :: ocean_state
      integer, intent(in) :: compute_rank
      integer, intent(out), optional :: ierr
         !! Non-zero on a vertical-mixing configuration conflict when
         !! present; absent behaves as today (`error stop`).

      ocean_state%dyn%enable_thermodynamics = cfg%ocean%thermo%enable_thermodynamics

      ! Velocity-truncation clamp (MOM6 MAXVEL); 0 = off.
      ocean_state%dyn%maxvel = cfg%ocean%pgf%maxvel
      if (compute_rank == 0) then
         if (cfg%ocean%pgf%maxvel > 0.0_wp) then
            call logger%info("Velocity clamp:   "//to_string(cfg%ocean%pgf%maxvel)//" m/s")
         end if
      end if

      ! Advective-CFL velocity truncation (E7, MOM6 CFL_trunc); 0 = off.
      ocean_state%dyn%cfl_trunc = cfg%ocean%pgf%cfl_trunc
      if (compute_rank == 0) then
         if (cfg%ocean%pgf%cfl_trunc > 0.0_wp) then
            call logger%info("CFL truncation:   threshold "//to_string(cfg%ocean%pgf%cfl_trunc))
         end if
      end if

      ! DIRECT_STRESS — wind stress spread over the top hmix_stress metres.
      ocean_state%surface_stress%direct_stress = cfg%ocean%vmix%direct_stress
      ocean_state%surface_stress%hmix_stress = cfg%ocean%vmix%hmix_stress
      if (compute_rank == 0) then
         if (cfg%ocean%vmix%direct_stress) then
            call logger%info("Surface stress:   DIRECT (over top "// &
                             to_string(cfg%ocean%vmix%hmix_stress)//" m)")
         end if
      end if

      ! KV_ML_INVZ2 — extra surface-band vertical viscosity (MOM6).
      ocean_state%vmix%kv_ml_invz2 = cfg%ocean%vmix%kv_ml_invz2
      ocean_state%vmix%hmix_fixed = cfg%ocean%vmix%hmix_fixed
      if (compute_rank == 0) then
         if (cfg%ocean%vmix%kv_ml_invz2 > 0.0_wp) then
            call logger%info("KV_ML_INVZ2:      "//to_string(cfg%ocean%vmix%kv_ml_invz2)// &
                             " m²/s in top "//to_string(cfg%ocean%vmix%hmix_fixed)//" m")
         end if
      end if

      ! C11 assembly gate — ceilings / smoothing / guard.  Backgrounds
      ! stay on the slot defaults (= pp81_*_bg, set in ocean_vmix_init)
      ! so the floor is a no-op for the closure path; only the ceilings /
      ! smoothing / guard are config-exposed.
      ocean_state%vmix%kv_max = cfg%ocean%vmix%kv_max
      ocean_state%vmix%kd_max = cfg%ocean%vmix%kd_max
      ocean_state%vmix%kd_smooth_iterations = cfg%ocean%vmix%kd_smooth_iterations
      ocean_state%vmix%vmix_guard = cfg%ocean%vmix%vmix_guard

      ! C7 Bryan-Lewis depth-varying background (default off = bit-identical).
      ocean_state%vmix%bkgnd_profile = cfg%ocean%vmix%bkgnd_profile
      ocean_state%vmix%bkgnd_kd_sfc = cfg%ocean%vmix%bkgnd_kd_sfc
      ocean_state%vmix%bkgnd_kd_deep = cfg%ocean%vmix%bkgnd_kd_deep
      ocean_state%vmix%bkgnd_z0 = cfg%ocean%vmix%bkgnd_z0
      ocean_state%vmix%bkgnd_delta = cfg%ocean%vmix%bkgnd_delta
      ocean_state%vmix%bkgnd_prandtl = cfg%ocean%vmix%bkgnd_prandtl
      ocean_state%vmix%bkgnd_henyey = cfg%ocean%vmix%bkgnd_henyey
      ocean_state%vmix%bkgnd_kd_min = cfg%ocean%vmix%bkgnd_kd_min
      ocean_state%vmix%bkgnd_henyey_n0_2omega = cfg%ocean%vmix%bkgnd_henyey_n0_2omega
      ocean_state%vmix%bkgnd_henyey_max_lat = cfg%ocean%vmix%bkgnd_henyey_max_lat
      ! Backstop for the two `validate_config` guards (Bryan-Lewis and Henyey
      ! mutually exclusive; non-cartesian grid_config required) — reached only
      ! by a caller that bypassed validation, since the namelist path stops
      ! there first.  Repeated here because a Henyey run with either
      ! precondition violated is silent (wrong background field), not noisy.
      if (bkgnd_henyey_conflicts_profile(cfg%ocean%vmix%bkgnd_henyey, &
                                         cfg%ocean%vmix%bkgnd_profile)) then
         call fail("ocean_vmix_nml: bkgnd_henyey and bkgnd_profile are "// &
                   "mutually exclusive background schemes — the Henyey factor "// &
                   "scales the SCALAR background, it does not compose with the "// &
                   "Bryan-Lewis depth profile", ierr, OCEAN_STATUS_ERR_SETUP)
         return
      end if
      if (cfg%ocean%vmix%bkgnd_henyey .and. &
          trim(cfg%ocean%grid%grid_config) == "cartesian") then
         call fail("ocean_vmix_nml: bkgnd_henyey requires a non-cartesian "// &
                   "grid_config — geolatT is identically zero on cartesian, so "// &
                   "every column would take the equatorial L(0 deg)=0 and the "// &
                   "background would flatten to a uniform bkgnd_kd_min", ierr, OCEAN_STATUS_ERR_SETUP)
         return
      end if
      if (compute_rank == 0) then
         if (cfg%ocean%vmix%bkgnd_profile) then
            call logger%info("Background mixing: Bryan-Lewis profile (Kd_sfc="// &
                             to_string(cfg%ocean%vmix%bkgnd_kd_sfc)//" Kd_deep="// &
                             to_string(cfg%ocean%vmix%bkgnd_kd_deep)//" z0="// &
                             to_string(cfg%ocean%vmix%bkgnd_z0)//" m)")
         end if
         if (cfg%ocean%vmix%bkgnd_henyey) then
            call logger%info("Background mixing: Henyey IGW latitude factor on the "// &
                             "scalar background (N0_2Omega="// &
                             to_string(cfg%ocean%vmix%bkgnd_henyey_n0_2omega)// &
                             " max_lat="//to_string(cfg%ocean%vmix%bkgnd_henyey_max_lat)// &
                             " degN Kd_min="// &
                             to_string(vmix_resolve_kd_min(cfg%ocean%vmix%bkgnd_kd_min, &
                                                           cfg%ocean%vmix%pp81_kappa_bg))// &
                             " m^2/s)")
            ! `pp81_kappa_bg` (not `ocean_state%vmix%kt_bg`) because
            ! `seed_backgrounds` — which re-derives kt_bg from it — runs
            ! later in this same routine; reading the slot here would log
            ! the stale type default whenever the user retuned kappa_bg.
         end if
      end if

      ! smooth_scratch is allocated ONLY when smoothing is requested —
      ! here, after kd_smooth_iterations is known, before enter_data.
      ! The enter_data path guards on allocated() so the not-allocated
      ! case (smoothing off) is handled correctly.
      if (cfg%ocean%vmix%kd_smooth_iterations > 0) then
         if (.not. allocated(ocean_state%vmix%smooth_scratch)) then
            associate (kv => ocean_state%vmix%kv)
               allocate (ocean_state%vmix%smooth_scratch( &
                         size(kv, 1), size(kv, 2), size(kv, 3)), source=0.0_wp)
            end associate
         end if
      end if

      if (compute_rank == 0) then
         if (cfg%ocean%vmix%kd_max < huge(1.0_wp) .or. cfg%ocean%vmix%kv_max < huge(1.0_wp)) then
            call logger%info("Vmix assembly:    kv_max="//to_string(cfg%ocean%vmix%kv_max)// &
                             " kd_max="//to_string(cfg%ocean%vmix%kd_max)//" m²/s")
         end if
         if (cfg%ocean%vmix%kd_smooth_iterations > 0) then
            call logger%info("Vmix assembly:    "// &
                             to_string(cfg%ocean%vmix%kd_smooth_iterations)// &
                             " 1-2-1 smoothing pass(es) on kv/kt (wet-mask aware)")
         end if
         if (cfg%ocean%vmix%vmix_guard) then
            call logger%info("Vmix assembly:    negative/NaN guard ON")
         end if
      end if

      ! HARMONIC_VISC — vdiff face-thickness mean (better at thin layers).
      ocean_state%vdiff%use_harmonic = cfg%ocean%vmix%harmonic_visc
      ! MOM6 HARMONIC_VISC parity for the momentum face thickness (hvel) +
      ! arithmetic h_shear.  Off by default => historical arithmetic h_u.
      ocean_state%vdiff%hvel_mom6 = cfg%ocean%vdiff%hvel_mom6
      ocean_state%vdiff%hbbl_visc = cfg%ocean%vdiff%hbbl_visc
      ocean_state%vdiff%hvel_harmonic = cfg%ocean%vdiff%hvel_harmonic
      ! MOM6 bottomdraglaw coupling parity: kv_bbl botfn glue + piston bed
      ! drag (PGF_BUG.md §9), with MOM6's per-face `set_viscous_BBL`
      ! (`vdiff_set_viscous_bbl`) built from the bottom-drag configuration:
      ! the drag law, `cd`/`r`, `bg_vel`, `bbl_thick_min`, and `HBBL` =
      ! `&ocean_bdrag_nml hbbl`, falling back to `&ocean_vdiff_nml
      ! hbbl_visc` for a drag configured bed-only (`hbbl = 0`) — MOM6 has
      ! ONE HBBL and no bed-only form.  No drag configured ⇒ the latch
      ! leaves the glue OFF.  `hvel_mom6` is validated at configure.
      ocean_state%vdiff%bbl_glue = cfg%ocean%vdiff%bbl_glue
      ocean_state%vdiff%bbl_piston = cfg%ocean%vdiff%bbl_piston
      ocean_state%vdiff%hvel_upwind = cfg%ocean%vdiff%hvel_upwind
      block
         integer :: bbl_form
         real(wp) :: bbl_hbbl
         bbl_form = BBL_FORM_QUADRATIC
         if (trim(cfg%ocean%bdrag%form) == "linear") bbl_form = BBL_FORM_LINEAR
         bbl_hbbl = cfg%ocean%bdrag%hbbl
         if (bbl_hbbl <= 0.0_wp) bbl_hbbl = cfg%ocean%vdiff%hbbl_visc
         call vdiff_bbl_configure(ocean_state%vdiff, &
                                  size(ocean_state%multilayer%h_layer, 1), &
                                  size(ocean_state%multilayer%h_layer, 2), &
                                  size(ocean_state%multilayer%h_layer, 3), &
                                  bbl_form, cfg%ocean%bdrag%cd, cfg%ocean%bdrag%r, &
                                  bbl_hbbl, cfg%ocean%bdrag%bg_vel, &
                                  cfg%ocean%bdrag%bbl_thick_min, &
                                  cfg%ocean%kshear%enable, ocean_state%eos%rho0, &
                                  cfg%ocean%vmix%pp81_nu_bg)
      end block
      if (compute_rank == 0 .and. cfg%ocean%vdiff%hvel_mom6 &
          .and. .not. cfg%ocean%vdiff%hvel_upwind) then
         call logger%info("vdiff: hvel near-bed upwind blend OFF (pure harmonic hvel)")
      end if
      if (compute_rank == 0 .and. cfg%ocean%vdiff%hvel_mom6) then
         if (cfg%ocean%vdiff%hvel_harmonic) then
            call logger%info("vdiff: MOM6 hvel, HARMONIC_VISC=True branch (harmonic + "// &
                             "near-bed upwind blend, arithmetic h_shear) ON")
         else
            call logger%info("vdiff: MOM6 hvel, HARMONIC_VISC=False branch (arithmetic, "// &
                             "z_clear near-bed harmonic blend, arithmetic h_shear) ON")
         end if
      end if
      if (compute_rank == 0 .and. ocean_state%vdiff%bbl_glue) then
         call logger%info("vdiff: MOM6 bottomdraglaw BBL glue ON (per-face "// &
                          "set_viscous_BBL kv_bbl/bbl_thick, "//trim(cfg%ocean%bdrag%form)// &
                          " law; botfn interfaces + piston bed row replace the "// &
                          "explicit bed-drag apply)")
      else if (compute_rank == 0 .and. cfg%ocean%vdiff%bbl_glue) then
         call logger%info("vdiff: bbl_glue requested but no bottom drag is configured "// &
                          "(&ocean_bdrag_nml cd/r = 0, or linear with bg_vel = 0) — glue OFF")
      end if
      if (compute_rank == 0) then
         if (cfg%ocean%vmix%harmonic_visc) then
            call logger%info("Vertical visc:    harmonic-mean face thickness")
         end if
      end if

      ! Implicit stress/drag folding into the vdiff tridiagonal
      ! (`&ocean_vdiff_nml`).  When on, the corresponding explicit
      ! pre-solve apply is gated off in run_stage (see rdb_ocean_dyn), and
      ! the bottom-drag slot fills the bed-layer Rayleigh-rate field
      ! `lambda_bot_u/v` that the vdiff diagonal consumes.  Mutual
      ! exclusions validated at configure (validate_config).
      ocean_state%vdiff%implicit_stress = cfg%ocean%vdiff%implicit_stress
      ocean_state%vdiff%implicit_drag = cfg%ocean%vdiff%implicit_drag
      ocean_state%bdrag%implicit_fold = cfg%ocean%vdiff%implicit_drag
      if (compute_rank == 0) then
         if (cfg%ocean%vdiff%implicit_stress) then
            call logger%info("Vertical visc:    wind stress folded into vdiff "// &
                             "surface (k=nz) RHS (implicit_stress)")
         end if
         if (cfg%ocean%vdiff%implicit_drag) then
            call logger%info("Vertical visc:    bottom drag folded into vdiff "// &
                             "bed (k=1) diagonal (implicit_drag)")
         end if
      end if

      ! DT_THERM ratio — thermo/tracer kernels fire every Nth outer step.
      ocean_state%dyn%dt_therm_ratio = max(1, cfg%ocean%vmix%dt_therm_ratio)
      if (compute_rank == 0) then
         if (ocean_state%dyn%dt_therm_ratio > 1) then
            call logger%info("Thermo ratio:     "// &
                             to_string(ocean_state%dyn%dt_therm_ratio)// &
                             "·dt (DT_THERM = ratio · dt_dynamic)")
         end if
      end if

      ! DT_TRACER_ADVECT ratio — horizontal tracer advect fires every
      ! Nth outer step over the accumulated face transports.  Must be
      ! >= 1 and dt_therm_ratio must be an integer multiple of it so the
      ! ALE remap (which fires at DT_THERM cadence) never lands inside an
      ! open accumulation window.  Fail loud, host-side.
      if (cfg%ocean%vmix%dt_tracer_advect_ratio < 1) then
         call fail("ocean_vmix_nml: dt_tracer_advect_ratio must be >= 1 (got "// &
                   to_string(cfg%ocean%vmix%dt_tracer_advect_ratio)//")", ierr, OCEAN_STATUS_ERR_SETUP)
         return
      end if
      ! `< 1` already error-stopped above, so the value is >= 1 here.
      ocean_state%dyn%dt_tracer_advect_ratio = cfg%ocean%vmix%dt_tracer_advect_ratio
      if (.not. ocean_dt_tracer_advect_ratios_ok(ocean_state%dyn%dt_therm_ratio, &
                                                 ocean_state%dyn%dt_tracer_advect_ratio)) then
         call fail("ocean_vmix_nml: dt_therm_ratio ("// &
                   to_string(ocean_state%dyn%dt_therm_ratio)// &
                   ") must be an integer multiple of dt_tracer_advect_ratio ("// &
                   to_string(ocean_state%dyn%dt_tracer_advect_ratio)// &
                   ") so the ALE remap never fires mid-accumulation-window", ierr, OCEAN_STATUS_ERR_SETUP)
         return
      end if
      if (compute_rank == 0) then
         if (ocean_state%dyn%dt_tracer_advect_ratio > 1) then
            call logger%info("Tracer advect:    "// &
                             to_string(ocean_state%dyn%dt_tracer_advect_ratio)// &
                             "·dt (DT_TRACER_ADVECT = ratio · dt_dynamic)")
         end if
      end if

      ! Q6: windowed-drain tracer face reconstruction (ppm | weno5/7/9).
      ! The per-rung nghost minimum is validated fail-loud in validate_config
      ! (cfg%nghost is the ocean grid nghost); here we only map the parsed
      ! code onto the continuity slot.  ppm (default) ⇒ bit-identical.
      ocean_state%continuity%tracer_recon = &
         parse_tracer_recon(trim(cfg%ocean%vmix%tracer_recon))
      if (compute_rank == 0 .and. &
          ocean_state%continuity%tracer_recon /= TRACER_RECON_PPM) then
         call logger%info("Tracer recon:     "//trim(cfg%ocean%vmix%tracer_recon)// &
                          " (windowed-drain WENO-Z swept-average face)")
      end if
      if (compute_rank == 0) then
         if (cfg%ocean%thermo%enable_thermodynamics) then
            call logger%info("Thermodynamics:   on")
         else
            call logger%info("Thermodynamics:   OFF (adiabatic — EOS/tracer kernels skipped)")
         end if
      end if
      if (present(ierr)) ierr = OCEAN_STATUS_OK
   end subroutine configure_ocean_vmix

   subroutine configure_ocean_tracers(cfg, ocean_state, compute_rank)
      !! `&ocean_tracers_nml` scalar knobs (PR-7): the ideal-age Dirichlet
      !! surface value and its vintage-mode exponential growth rate.
      !! `enable_ideal_age` itself is latched earlier by
      !! `ocean_state_copy_config` (before `init(grid)`, since it gates
      !! tracer-slot allocation) — these two scalars gate nothing, so
      !! they land here in the post-init configure pass.  Both default
      !! to 0 ⇒ bit-identical.
      type(config_t), intent(in) :: cfg
      type(ocean_state_t), intent(inout) :: ocean_state
      integer, intent(in) :: compute_rank

      ocean_state%dyn%ideal_age_young_val = cfg%ocean%tracers%ideal_age_young_val
      ocean_state%dyn%ideal_age_sfc_growth_rate = cfg%ocean%tracers%ideal_age_sfc_growth_rate
      if (compute_rank == 0 .and. cfg%ocean%tracers%enable_ideal_age) then
         call logger%info("Ideal age:        surface reset after rk2_average + remap")
         if (cfg%ocean%tracers%ideal_age_sfc_growth_rate /= 0.0_wp) then
            call logger%info("Ideal age:        vintage mode, young_val="// &
                             to_string(cfg%ocean%tracers%ideal_age_young_val)// &
                             " growth_rate="// &
                             to_string(cfg%ocean%tracers%ideal_age_sfc_growth_rate)//" 1/s")
         end if
      end if
   end subroutine configure_ocean_tracers

   subroutine configure_ocean_lateral(cfg, ocean_state, grid, compute_rank, ierr)
      !! Flow-aware lateral-viscosity closure (Leith / Smagorinsky + biharmonic
      !! Smagorinsky_AH), the vertical coordinate (VCOORD_* code + z_fixed
      !! reference depth), and the PP81/KPP vertical-mixing switches — with
      !! their rank-0 log lines.
      type(config_t), intent(in) :: cfg
      type(ocean_state_t), intent(inout) :: ocean_state
      type(hgrid_t), intent(in) :: grid
      integer, intent(in) :: compute_rank
      integer, intent(out), optional :: ierr
         !! Non-zero when any of the sub-closures configured here
         !! (EPBL/kappa-shear/tidal-mixing/conv/ddiff/wavespeed/Fox-Kemper)
         !! rejects the resolved configuration, when present; absent
         !! behaves as today (`error stop`).
      integer :: local_ierr

      ! Lateral closure + background/cap coefficients.  ah_bg defaults to the
      ! MOM6 floor max(nu_h, kh_vel_scale·dx) when cfg leaves it negative.
      ocean_state%lateral_mix%closure = parse_lateral_closure(cfg%ocean%hvisc%lateral_closure)
      ocean_state%lateral_mix%no_slip = cfg%ocean%hvisc%no_slip
      ocean_state%lateral_mix%c_smag = cfg%ocean%hvisc%c_smag
      ocean_state%lateral_mix%c_leith = cfg%ocean%hvisc%c_leith
      if (cfg%ocean%hvisc%ah_bg >= 0.0_wp) then
         ocean_state%lateral_mix%ah_bg = cfg%ocean%hvisc%ah_bg
      else
         ocean_state%lateral_mix%ah_bg = &
            max(cfg%ocean%hvisc%nu_h, cfg%ocean%hvisc%kh_vel_scale* &
                metrics_dx_min(ocean_state%metrics, grid))
      end if
      ocean_state%lateral_mix%ah_max = cfg%ocean%hvisc%ah_max
      ocean_state%lateral_mix%kh_vel_scale_live = cfg%ocean%hvisc%kh_vel_scale_live
      ocean_state%lateral_mix%smag_ah_active = cfg%ocean%hvisc%smag_ah
      ocean_state%lateral_mix%smag_bi_const = cfg%ocean%hvisc%smag_bi_const
      ocean_state%lateral_mix%c_leith_bi = cfg%ocean%hvisc%c_leith_bi
      ocean_state%lateral_mix%nu4_bg = cfg%ocean%hvisc%nu_4_bg
      ocean_state%lateral_mix%nu4_max = cfg%ocean%hvisc%nu_4_max
      ocean_state%lateral_mix%resoln_scaled_visc = cfg%ocean%hvisc%resoln_scaled_visc
      if (compute_rank == 0) then
         select case (ocean_state%lateral_mix%closure)
         case (LMIX_NONE)
            call logger%info("Lateral closure:  none (scalar ocean_nu_h)")
         case (LMIX_LEITH)
            call logger%info("Lateral closure:  Leith  C_L="//to_string(cfg%ocean%hvisc%c_leith)// &
                             "  ah_bg="//to_string(ocean_state%lateral_mix%ah_bg)// &
                             "  ah_max="//to_string(cfg%ocean%hvisc%ah_max)//" m²/s")
         case (LMIX_SMAGORINSKY)
            call logger%info("Lateral closure:  Smagorinsky  C_S="//to_string(cfg%ocean%hvisc%c_smag)// &
                             "  ah_bg="//to_string(ocean_state%lateral_mix%ah_bg)// &
                             "  ah_max="//to_string(cfg%ocean%hvisc%ah_max)//" m²/s")
         case (LMIX_LEITH_BIHARM)
            call logger%info("Lateral closure:  Leith-biharmonic  C_lb="//to_string(cfg%ocean%hvisc%c_leith_bi)// &
                             "  nu4_bg="//to_string(ocean_state%lateral_mix%nu4_bg)// &
                             "  nu4_max="//to_string(cfg%ocean%hvisc%nu_4_max)//" m⁴/s")
            ! PR-6: the c_leith_bi<=0 "inert" case is now a configure-time
            ! ABORT in validate_config (leith_biharm_is_inert), so setup is
            ! never reached with an inert leith_biharm — the warning here
            ! would be dead code.
         case (LMIX_BIHARMONIC)
            call logger%info("Lateral closure:  constant biharmonic  nu_4="//to_string(cfg%ocean%hvisc%nu_4)//" m⁴/s")
         case default
            ! LMIX_NONE handled above; any other code is rejected at
            ! configure (validate_config), so this is unreachable.
            call logger%info("Lateral closure:  "//trim(cfg%ocean%hvisc%lateral_closure))
         end select
         if (ocean_state%lateral_mix%smag_ah_active) then
            call logger%info("Biharmonic closure: Smagorinsky_AH  C_b="// &
                             to_string(cfg%ocean%hvisc%smag_bi_const)// &
                             "  nu4_bg="//to_string(cfg%ocean%hvisc%nu_4_bg)// &
                             "  nu4_max="//to_string(cfg%ocean%hvisc%nu_4_max)//" m⁴/s")
         end if
      end if

      ! Vertical coordinate: namelist string → VCOORD_* code; z_fixed needs a
      ! reference total depth.  Without this every vcoord_type silently ran
      ! Eulerian-z (no ALE remap).
      ocean_state%vcoord%coord_type = parse_ocean_vcoord_type(cfg%vcoord_type)
      ocean_state%vcoord%remap_method = parse_remap_method(cfg%remap_method)
      call configure_ocean_z_fixed_profile(cfg, ocean_state, compute_rank, log_it=.false.)
      ! Isopycnal (VCOORD_RHO) target densities: a uniform light->dense
      ! linspace from rho_target_light/dense (MOM6 ALE_COORDINATE_CONFIG=
      ! UNIFORM analogue).  `rho_target(0)` is the lightest (surface)
      ! interface, `rho_target(nz)` the densest (bed).
      ocean_state%vcoord%rho_ref_pressure = cfg%rho_ref_pressure
      if (allocated(ocean_state%vcoord%rho_target)) then
         call configure_rho_target(cfg, ocean_state%vcoord%rho_target, ocean_state%vcoord%nz_ml)
      end if
      ! ALE-regrid refinements (all default-off ⇒ bit-identical).
      ocean_state%vcoord%regrid_time_scale = cfg%regrid_time_scale
      ocean_state%vcoord%remap_vel_conserve_ke = cfg%remap_vel_conserve_ke
      ocean_state%vcoord%remap_boundary_extrap = cfg%remap_boundary_extrap
      ocean_state%vcoord%remap_nonuniform_weights = cfg%remap_nonuniform_weights
      ocean_state%vcoord%remap_check_preconditions = cfg%remap_check_preconditions
      ocean_state%vcoord%check_vanished_content = cfg%check_vanished_content
      if (compute_rank == 0) then
         if (ocean_state%vcoord%coord_type == VCOORD_EULERIAN_Z) then
            call logger%info("Vertical coord:  "//trim(cfg%vcoord_type)// &
                             " → EULERIAN_Z (no ALE remap)")
         else if (ocean_state%vcoord%coord_type == VCOORD_RHO) then
            ! RHO is state-dependent: the regrid silently falls back to a
            ! geometric grid if S/T or the EOS handle are unavailable at
            ! remap time.  Make activation + its requirement visible here
            ! (a hard configure-time S/T check is a deferred follow-up —
            ! S/T are always registered on the default ocean path).
            call logger%info("Vertical coord:  rho (isopycnal, ALE remap; "// &
                             "validation-grade — needs S+T + EOS; rho_ref_p="// &
                             to_string(cfg%rho_ref_pressure)//" Pa, targets "// &
                             to_string(ocean_state%vcoord%rho_target(0))//" .. "// &
                             to_string(ocean_state%vcoord%rho_target( &
                                       ocean_state%vcoord%nz_ml))//" kg/m^3 ("// &
                             trim(cfg%rho_target_profile)//"))")
         else if (ocean_state%vcoord%coord_type == VCOORD_HYCOM) then
            ! HYCOM = the RHO density-space inversion + a z* nominal-floor
            ! sweep (fixed-resolution surface band, isopycnal interior).
            ! Reuses the RHO rho_target + EOS; the floor is the z* nominal
            ! profile in metres (z_fixed_profile).  Needs S+T.
            call logger%info("Vertical coord:  hycom (hybrid z*/isopycnal, ALE "// &
                             "remap; needs S+T + EOS; rho_ref_p="// &
                             to_string(cfg%rho_ref_pressure)//" Pa, targets "// &
                             to_string(ocean_state%vcoord%rho_target(0))//" .. "// &
                             to_string(ocean_state%vcoord%rho_target( &
                                       ocean_state%vcoord%nz_ml))//" kg/m^3 ("// &
                             trim(cfg%rho_target_profile)//"); z* floor "// &
                             trim(cfg%z_fixed_profile)//", top dz "// &
                             to_string(hycom_top_dz(ocean_state%vcoord))//" m)")
         else
            call logger%info("Vertical coord:  "//trim(cfg%vcoord_type)// &
                             " (code "//to_string(ocean_state%vcoord%coord_type)// &
                             ", ALE remap enabled)  h_surf_target="// &
                             to_string(cfg%zstar_h_surf_target)//" m")
         end if
         ! Echo the IC thickness seed only when it is NOT the default, so the
         ! banner stays unchanged for every existing config.  Worth surfacing:
         ! "uniform_z" changes the resting isopycnal geometry, not just a
         ! tolerance, and is otherwise invisible after t=0.
         if (trim(cfg%thickness_config) /= "sigma") then
            call logger%info("Initial thickness: "//trim(cfg%thickness_config)// &
                             " (MOM6 uniform-z interfaces over max_depth="// &
                             to_string(cfg%ocean%topo%max_depth)// &
                             " m, clipped to bathymetry; flat resting isopycnals)")
         end if
      end if

      ! Vertical mixing: PP81 interior + KPP boundary-layer overlay.
      ocean_state%vmix%use_closure = cfg%ocean%vmix%use_closure
      ocean_state%vmix%use_kpp = cfg%ocean%vmix%use_kpp
      ! Shared EOS handle: KPP B0 reads the surface α/β from here so
      ! it tracks the dyn-core EOS (was a private never-refreshed copy).
      ocean_state%vmix%eos = ocean_state%eos

      ! PP81 interior closure + KPP BL-depth constants (`&ocean_vmix_nml
      ! pp81_*`/`kpp_*`).  Each default equals the ocean_vmix_t field
      ! default, so an nml that never mentions these keys is bit-identical.
      ocean_state%vmix%pp81_nu0 = cfg%ocean%vmix%pp81_nu0
      ocean_state%vmix%pp81_nu_bg = cfg%ocean%vmix%pp81_nu_bg
      ocean_state%vmix%pp81_kappa_bg = cfg%ocean%vmix%pp81_kappa_bg
      ocean_state%vmix%pp81_alpha = cfg%ocean%vmix%pp81_alpha
      ocean_state%vmix%shear2_floor = cfg%ocean%vmix%shear2_floor
      ocean_state%vmix%ri_crit = cfg%ocean%vmix%kpp_ri_crit
      ocean_state%vmix%cs_nonlocal = cfg%ocean%vmix%kpp_cs_nonlocal
      ocean_state%vmix%c_vt2 = cfg%ocean%vmix%kpp_c_vt2
      ! (PR-21) KPP shortwave-in-BL method (&ocean_thermo_nml
      ! kpp_sw_method).  validate_config already rejected unknown strings.
      ocean_state%vmix%kpp_sw_method = &
         parse_kpp_sw_method(trim(cfg%ocean%thermo%kpp_sw_method))
      ! (E4) Source of the α/β pair behind the KPP `B_0` and the
      ! double-diffusion density ratio (`&ocean_vmix_nml
      ! buoyancy_coeffs`).  `nml_enum allowed=` already rejected an
      ! unknown spelling at parse time; the INVALID arm here is the
      ! belt-and-braces guard that keeps the schema list and
      ! `parse_buoyancy_coeffs` from drifting apart silently (a wrong α is
      ! a physics change with no symptom, so never a fallback).  Both this
      ! and the `p_top` gate below are plain host scalars latched BEFORE
      ! `ocean_state_enter_data`'s `copyin`, which is the same contract
      ! `rho0` and the `pp81_*` scalars beside them rely on.
      ocean_state%vmix%buoyancy_coeffs = &
         parse_buoyancy_coeffs(trim(cfg%ocean%vmix%buoyancy_coeffs))
      if (ocean_state%vmix%buoyancy_coeffs == BUOY_COEFFS_INVALID) then
         call fail("ocean_vmix_nml: buoyancy_coeffs='"// &
                   trim(cfg%ocean%vmix%buoyancy_coeffs)// &
                   "' is not a known source; use 'constant' or 'eos'", &
                   ierr, OCEAN_STATUS_ERR_SETUP)
         return
      end if
      ! Mirror of the E3 `&ocean_psurf_nml in_eos` seam gate, taken from
      ! the SAME knob EPBL's `in_eos` takes so the two boundary-layer
      ! schemes measure their in-situ pressures from the same origin.
      ! NOT redundant with `p_top` being zero — a cavity fills `p_top`
      ! with the ice load whether or not `in_eos` is set.
      ocean_state%vmix%p_top_in_eos = cfg%ocean%psurf%in_eos
      ! MANDATORY re-derive: kv_bg/kt_bg/ks_bg and the kt/ks/kd_bg arrays
      ! were set from the TYPE-DEFAULT pp81_* at `init` time (before this
      ! configure call runs); without this, `vmix_assemble`'s background
      ! floor would silently clamp against the old default even though
      ! the user just set a new pp81_nu_bg/pp81_kappa_bg, and a
      ! Bryan-Lewis/Henyey background config would lose `kd_bg` entirely
      ! on every warm restart (`kd_bg` is set ONLY here) — see
      ! `vmix_seed_backgrounds`'s docstring.  `kv` alone is skipped when
      ! `engine_setup`'s restart read actually found it in the checkpoint
      ! (PR-2, bt-rem-from-av-rem: `vmix%kv_from_restart`, set by
      ! `ocean_state_restart_read`) -- it is the one CARRIED field here
      ! (`visc_rem_precompute` reads the PREVIOUS stage's `kv`), so an
      ! unconditional reseed would stomp the just-restored value back to
      ! the cold background on every resume; a cold start or an older
      ! checkpoint without `vmix_kv` leaves `kv_from_restart = .false.`
      ! and `kv` still reseeds normally.  Runs before
      ! `ocean_state_enter_data` (driver), so no `!$acc update` is needed
      ! either way -- the host array this leaves in place (restored or
      ! freshly seeded) is what the later `copyin` maps.
      call ocean_state%vmix%seed_backgrounds(skip_kv=ocean_state%vmix%kv_from_restart)

      ! EPBL — energetics-based PBL.  Replaces the KPP overlay when
      ! enabled (mutually exclusive surface schemes); the PP81
      ! interior + background continue underneath.
      if (present(ierr)) then
         call configure_ocean_epbl(cfg, ocean_state, grid, compute_rank, ierr=local_ierr)
         if (local_ierr /= 0) then
            ierr = local_ierr
            return
         end if
      else
         call configure_ocean_epbl(cfg, ocean_state, grid, compute_rank)
      end if

      ! Kappa-shear — JHL08 shear-driven INTERIOR closure.  Coexists
      ! with KPP and EPBL (no mutual exclusion); its kappa is added to
      ! the interior diffusivities.
      if (present(ierr)) then
         call configure_ocean_kappa_shear(cfg, ocean_state, grid, compute_rank, ierr=local_ierr)
         if (local_ierr /= 0) then
            ierr = local_ierr
            return
         end if
      else
         call configure_ocean_kappa_shear(cfg, ocean_state, grid, compute_rank)
      end if

      ! Tidal mixing — St-Laurent/Simmons internal-tide INTERIOR closure.
      ! Coexists with KPP/EPBL and PP81/background/kappa-shear (no mutual
      ! exclusion); its Kd is added to the interior diffusivities.
      if (present(ierr)) then
         call configure_ocean_tidal_mixing(cfg, ocean_state, compute_rank, ierr=local_ierr)
         if (local_ierr /= 0) then
            ierr = local_ierr
            return
         end if
      else
         call configure_ocean_tidal_mixing(cfg, ocean_state, compute_rank)
      end if

      ! Convective adjustment — Brunt-Vaisala-triggered INTERIOR closure
      ! CONTRIBUTOR (max() floor on kv/kt).  Coexists with KPP/EPBL (masks
      ! against whichever BL depth is live); no mutual exclusion.
      if (present(ierr)) then
         call configure_ocean_conv(cfg, ocean_state, compute_rank, ierr=local_ierr)
         if (local_ierr /= 0) then
            ierr = local_ierr
            return
         end if
      else
         call configure_ocean_conv(cfg, ocean_state, compute_rank)
      end if

      ! Double diffusion — salt fingering + diffusive convection, folded
      ! INTO the heat/salt split (asymmetric ks vs kt).  Reads the mirrored
      ! vmix%eos coefficients set above.
      if (present(ierr)) then
         call configure_ocean_ddiff(cfg, ocean_state, compute_rank, ierr=local_ierr)
         if (local_ierr /= 0) then
            ierr = local_ierr
            return
         end if
      else
         call configure_ocean_ddiff(cfg, ocean_state, compute_rank)
      end if

      ! Wave speed — B1 first-baroclinic cg1 + Rd diagnostic.
      if (present(ierr)) then
         call configure_ocean_wavespeed(cfg, ocean_state, grid, ierr=local_ierr)
         if (local_ierr /= 0) then
            ierr = local_ierr
            return
         end if
      else
         call configure_ocean_wavespeed(cfg, ocean_state, grid)
      end if
      ! Fox-Kemper MLE restratification (B5) — reads epbl%mld; configure
      ! after EPBL so the enable check sees the resolved EPBL state.
      if (present(ierr)) then
         call configure_ocean_foxkemper(cfg, ocean_state, compute_rank, ierr=local_ierr)
         if (local_ierr /= 0) then
            ierr = local_ierr
            return
         end if
      else
         call configure_ocean_foxkemper(cfg, ocean_state, compute_rank)
      end if

      ! VarMix (capability [4]) — build the STATIC f2_dx2/beta_dx2/L2 face
      ! terms once from the (filled) metrics + cell-centre Coriolis.  Runs
      ! after wavespeed (cg1) + metrics; the per-step Res_fn/SN/assembly
      ! fire at thermo cadence in the dyn step.  No-op when disabled.
      call configure_ocean_varmix(cfg, ocean_state, grid)

      ! MEKE (capability [5]) — fill the cell-centre |f| from the same
      ! Coriolis path VarMix/EPBL use, so the Rhines length is live when
      ! alpha_rhines > 0.  No-op when MEKE is disabled.  Scalar knobs are
      ! copied earlier by `ocean_state_copy_config`.
      call configure_ocean_meke(cfg, ocean_state, grid)

      if (compute_rank == 0) then
         if (ocean_state%epbl%enable) then
            call logger%info("Vertical mixing: PP81 interior + EPBL boundary layer")
         else if (ocean_state%vmix%use_closure .and. ocean_state%vmix%use_kpp) then
            call logger%info("Vertical mixing: PP81 interior + KPP overlay enabled")
         else if (ocean_state%vmix%use_closure) then
            call logger%info("Vertical mixing: PP81 interior only (KPP off)")
         else
            call logger%info("Vertical mixing: scalar K_v fallback (closure disabled)")
         end if
         if (cfg%ocean%vmix%pp81_nu0 /= 1.0e-2_wp .or. cfg%ocean%vmix%pp81_nu_bg /= 1.0e-4_wp .or. &
             cfg%ocean%vmix%pp81_kappa_bg /= 1.0e-5_wp .or. cfg%ocean%vmix%pp81_alpha /= 5.0_wp .or. &
             cfg%ocean%vmix%shear2_floor /= 1.0e-10_wp) then
            call logger%info("PP81 closure:     nu0="//to_string(cfg%ocean%vmix%pp81_nu0)// &
                             "  nu_bg="//to_string(cfg%ocean%vmix%pp81_nu_bg)// &
                             "  kappa_bg="//to_string(cfg%ocean%vmix%pp81_kappa_bg)// &
                             "  alpha="//to_string(cfg%ocean%vmix%pp81_alpha)// &
                             "  shear2_floor="//to_string(cfg%ocean%vmix%shear2_floor))
         end if
         if (cfg%ocean%vmix%kpp_ri_crit /= 0.3_wp .or. cfg%ocean%vmix%kpp_cs_nonlocal /= 6.3_wp .or. &
             cfg%ocean%vmix%kpp_c_vt2 /= 1.8_wp) then
            call logger%info("KPP constants:    ri_crit="//to_string(cfg%ocean%vmix%kpp_ri_crit)// &
                             "  cs_nonlocal="//to_string(cfg%ocean%vmix%kpp_cs_nonlocal)// &
                             "  c_vt2="//to_string(cfg%ocean%vmix%kpp_c_vt2))
         end if
      end if
      if (present(ierr)) ierr = OCEAN_STATUS_OK
   end subroutine configure_ocean_lateral

   pure function hycom_top_dz(vcoord) result(dz)
      !! Surface-layer thickness of the `hycom` z* nominal floor (m), for
      !! the configure banner: the stretched profile's top entry, else the
      !! uniform `z_fixed_h_ref/nz`, else 0 (the unconfigured sigma
      !! fallback of `ocean_vcoord_rho_target_column`).
      type(ocean_vcoord_t), intent(in) :: vcoord
      real(wp) :: dz
      dz = 0.0_wp
      if (vcoord%z_fixed_use_profile) then
         dz = vcoord%z_fixed_dz(vcoord%nz_ml)
      else if (vcoord%z_fixed_h_ref > 0.0_wp .and. vcoord%nz_ml > 0) then
         dz = vcoord%z_fixed_h_ref/real(vcoord%nz_ml, wp)
      end if
   end function hycom_top_dz

   subroutine configure_rho_target(cfg, rho_target, nz_ml)
      !! Populate the isopycnal `rho_target(0:nz_ml)` interface densities
      !! for `VCOORD_RHO` / `VCOORD_HYCOM`.  `rho_target(0)` is the surface
      !! (lightest) interface, `rho_target(nz_ml)` the bed (densest).
      !!
      !!   * `&vcoord_nml rho_target_profile = "uniform"` (default) — a
      !!     uniform light→dense linspace from `rho_target_light` /
      !!     `rho_target_dense` (MOM6 `ALE_COORDINATE_CONFIG=UNIFORM`);
      !!   * `"list"` — the `nz_ml+1` interface densities of
      !!     `rho_target_list`, light first (MOM6 target densities from a
      !!     list or a file, e.g. `HYBRID:file,sigma2,dz`).
      !!
      !! `validate_config` has already refused a list of the wrong length
      !! or one that is not strictly increasing; a short list reaching here
      !! is an `error stop`.  Harmless on other families.
      type(config_t), intent(in) :: cfg
      real(wp), intent(inout) :: rho_target(0:)
      integer, intent(in) :: nz_ml
      integer :: k
      real(wp) :: frac

      if (trim(cfg%rho_target_profile) == "list") then
         ! validate_config refuses a short list; reaching here with one is
         ! a bug, so stop rather than fall back to the linspace.
         if (count(cfg%rho_target_list > 0.0_wp) < nz_ml + 1) then
            error stop "configure_rho_target: rho_target_list shorter than nz_ml+1"
         end if
         do k = 0, nz_ml
            rho_target(k) = cfg%rho_target_list(k + 1)
         end do
         return
      end if
      do k = 0, nz_ml
         if (nz_ml > 0) then
            frac = real(k, wp)/real(nz_ml, wp)
         else
            frac = 0.0_wp
         end if
         rho_target(k) = cfg%rho_target_light &
                         + (cfg%rho_target_dense - cfg%rho_target_light)*frac
      end do
   end subroutine configure_rho_target

   subroutine configure_ocean_epbl(cfg, ocean_state, grid, compute_rank, ierr)
      !! Copy the `&ocean_epbl_nml` knobs onto the EPBL slot — every
      !! field, end to end (don't repeat the KPP ri_crit/c_vt2
      !! dead-config gap).  Also fills `f_centre` from the same
      !! beta-plane parameters the Coriolis slot uses, copies the EOS
      !! hookup, validates the configuration, and resolves the
      !! EPBL-vs-KPP mutual exclusion.
      type(config_t), intent(in) :: cfg
      type(ocean_state_t), intent(inout) :: ocean_state
      type(hgrid_t), intent(in) :: grid
      integer, intent(in) :: compute_rank
      integer, intent(out), optional :: ierr
         !! Non-zero on an EPBL configuration conflict when present; absent
         !! behaves as today (`error stop`).

      associate (epbl => ocean_state%epbl, ecfg => cfg%ocean%epbl)
         ! `enable` is flipped LAST, only once every validation below has
         ! passed (P0.1 review F9 — `configure_ocean_porous` is the
         ! pattern): setting it up front left a half-configured slot
         ! flagged enabled on any of this routine's many failure returns.
         if (.not. ecfg%enable) then
            epbl%enable = .false.
            if (present(ierr)) ierr = OCEAN_STATUS_OK
            return
         end if

         epbl%mstar_scheme = parse_epbl_mstar_scheme(ecfg%mstar_scheme)
         if (epbl%mstar_scheme < 0) then
            call fail("ocean_epbl_nml: unknown mstar_scheme '"// &
                      trim(ecfg%mstar_scheme)//"' (constant/om4/rh18)", ierr, OCEAN_STATUS_ERR_SETUP)
            return
         end if
         epbl%vstar_scheme = parse_epbl_vstar_scheme(ecfg%vel_scale_scheme)
         if (epbl%vstar_scheme < 0) then
            call fail("ocean_epbl_nml: unknown vel_scale_scheme '"// &
                      trim(ecfg%vel_scale_scheme)//"' (cube_root/rh18)", ierr, OCEAN_STATUS_ERR_SETUP)
            return
         end if
         epbl%combine_mode = parse_epbl_combine(ecfg%combine)
         if (epbl%combine_mode < 0) then
            call fail("ocean_epbl_nml: unknown combine '"// &
                      trim(ecfg%combine)//"' (add/max)", ierr, OCEAN_STATUS_ERR_SETUP)
            return
         end if

         epbl%mstar_const = ecfg%mstar
         epbl%mstar_cap = ecfg%mstar_cap
         epbl%mstar_coef1 = ecfg%mstar_coef1
         epbl%c_ek = ecfg%c_ek
         epbl%mstar_conv_adj = ecfg%mstar_conv_adj
         epbl%rh18_cn1 = ecfg%rh18_cn1
         epbl%rh18_cn2 = ecfg%rh18_cn2
         epbl%rh18_cn3 = ecfg%rh18_cn3
         epbl%rh18_cs1 = ecfg%rh18_cs1
         epbl%rh18_cs2 = ecfg%rh18_cs2
         epbl%nstar = ecfg%nstar
         epbl%tke_decay = ecfg%tke_decay
         epbl%wstar_ustar_coef = ecfg%wstar_ustar_coef
         epbl%vstar_scale_fac = ecfg%vstar_scale_fac
         epbl%vstar_surf_fac = ecfg%vstar_surf_fac
         epbl%von_karman = ecfg%von_karman
         epbl%ekman_scale_coef = ecfg%ekman_scale_coef
         epbl%min_mix_len = ecfg%min_mix_len
         epbl%mixlen_exponent = ecfg%mixlen_exponent
         epbl%translay_scale = ecfg%translay_scale
         epbl%mld_iteration = ecfg%mld_iteration
         epbl%mld_tol = ecfg%mld_tol
         epbl%mld_max_its = ecfg%mld_max_its
         epbl%mld_bisection = ecfg%mld_bisection
         epbl%mld_use_prev_guess = ecfg%mld_use_prev_guess
         epbl%omega = ecfg%omega
         epbl%omega_frac = ecfg%omega_frac
         epbl%prandtl = ecfg%prandtl
         epbl%tke_diags = ecfg%tke_diags
         ! (PR-21) Penetrating-SW TKE ledger switch lives on
         ! &ocean_thermo_nml (shared with KPP), not &ocean_epbl_nml.
         epbl%epbl_sw_ctke = cfg%ocean%thermo%epbl_sw_ctke

         ! Langmuir (LF17 wind-only path).
         epbl%use_lt = ecfg%use_lt
         if (ecfg%use_lt) then
            epbl%lt_scheme = parse_epbl_lt_scheme(ecfg%lt_scheme)
            if (epbl%lt_scheme < 0) then
               call fail("ocean_epbl_nml: unknown lt_scheme '"// &
                         trim(ecfg%lt_scheme)//"' (rescale/additive)", ierr, OCEAN_STATUS_ERR_SETUP)
               return
            end if
            epbl%lt_enhance_coef = ecfg%lt_enhance_coef
            epbl%lt_enhance_exp = ecfg%lt_enhance_exp
            epbl%lt_max_enhance = ecfg%lt_max_enhance
            epbl%la_frac_hbl = ecfg%la_frac_hbl
            epbl%lt_lac1 = ecfg%lt_lac1
            epbl%lt_lac2 = ecfg%lt_lac2
            epbl%lt_lac3 = ecfg%lt_lac3
            epbl%lt_lac4 = ecfg%lt_lac4
            epbl%lt_lac5 = ecfg%lt_lac5
            if (compute_rank == 0) then
               call logger%info("EPBL Langmuir:    LF17 wind-only, scheme="// &
                                trim(ecfg%lt_scheme)//"  coef="// &
                                to_string(ecfg%lt_enhance_coef)//"  exp="// &
                                to_string(ecfg%lt_enhance_exp))
            end if
         end if

         ! EOS hookup: the EPBL energy weights use the SAME EOS handle
         ! the dyn-core runs (shared flat-POD copy — one source of
         ! truth, no scalar copies that can drift).
         epbl%eos = ocean_state%eos
         epbl%rho0 = ocean_state%eos%rho0

         ! (E3) Top-of-column pressure in the IN-SITU EOS arguments AND
         ! in the PE weight.  `&ocean_psurf_nml in_eos` is the single
         ! gate for the whole `ms%p_top` seam; assigned HERE, at
         ! configure, so it is latched before `ocean_state_enter_data`
         ! and the kernel reads it by value (host scalar -- no
         ! `!$acc update device` owed).  Off (default) ⇒ the stack
         ! starts at 0 Pa, bit-identically.  NOTE this is NOT redundant
         ! with `p_top` being zero: a cavity fills `p_top` with the ice
         ! load whether or not `in_eos` is set.
         epbl%in_eos = cfg%ocean%psurf%in_eos

         ! |f| at cell centres, same scheme the Coriolis slot uses (D7).
         call fill_coriolis_centre(cfg, ocean_state%metrics, grid, epbl%f_centre)

         ! Validation.
         if (.not. cfg%ocean%vmix%use_closure) then
            call fail("ocean_epbl_nml: enable=.true. requires "// &
                      "ocean_vmix_nml use_closure=.true. (EPBL folds "// &
                      "into the interior closure's kv/kt)", ierr, OCEAN_STATUS_ERR_SETUP)
            return
         end if
         if (.not. cfg%ocean%thermo%enable_thermodynamics) then
            call fail("ocean_epbl_nml: enable=.true. requires "// &
                      "active thermodynamics (EPBL reads T, S)", ierr, OCEAN_STATUS_ERR_SETUP)
            return
         end if
         if (epbl%mld_iteration .and. &
             (epbl%translay_scale < 0.0_wp .or. epbl%translay_scale >= 1.0_wp)) then
            call fail("ocean_epbl_nml: translay_scale must be in "// &
                      "[0,1) when mld_iteration is on", ierr, OCEAN_STATUS_ERR_SETUP)
            return
         end if
         ! KPP mutual exclusion: EPBL replaces the overlay.  use_kpp
         ! defaults on, so override with a log line rather than abort.
         if (ocean_state%vmix%use_kpp) then
            ocean_state%vmix%use_kpp = .false.
            if (compute_rank == 0) then
               call logger%info("EPBL enabled: KPP overlay disabled "// &
                                "(mutually exclusive surface schemes)")
            end if
         end if

         if (compute_rank == 0) then
            call logger%info("EPBL:             mstar="//trim(ecfg%mstar_scheme)// &
                             "  nstar="//to_string(ecfg%nstar)// &
                             "  tke_decay="//to_string(ecfg%tke_decay)// &
                             "  vstar="//trim(ecfg%vel_scale_scheme)// &
                             "  combine="//trim(ecfg%combine))
         end if
         epbl%enable = .true.
      end associate
      if (present(ierr)) ierr = OCEAN_STATUS_OK
   end subroutine configure_ocean_epbl

   subroutine configure_ocean_foxkemper(cfg, ocean_state, compute_rank, ierr)
      !! Copy the `&ocean_foxkemper_nml` knobs onto the MLE slot (B5).
      !! Validates: B5 reads `epbl%mld`, so EPBL must be enabled; and the
      !! `resolution_taper` hook is a hard error until B2 lands.
      type(config_t), intent(in) :: cfg
      type(ocean_state_t), intent(inout) :: ocean_state
      integer, intent(in) :: compute_rank
      integer, intent(out), optional :: ierr
         !! Non-zero on a Fox-Kemper MLE configuration conflict when
         !! present; absent behaves as today (`error stop`).

      associate (mle => ocean_state%mle, fcfg => cfg%ocean%foxkemper)
         mle%enable = fcfg%enable
         if (.not. fcfg%enable) then
            if (present(ierr)) ierr = OCEAN_STATUS_OK
            return
         end if

         mle%ce = fcfg%ce
         mle%f_floor = fcfg%f_floor
         mle%mld_decay_time = fcfg%mld_decay_time
         mle%tail_dh = fcfg%tail_dh
         mle%use_mom_mixrate = fcfg%use_mom_mixrate
         mle%resolution_taper = fcfg%resolution_taper
         mle%use_bodner = fcfg%use_bodner
         mle%cr = fcfg%cr
         mle%bodner_mstar = fcfg%bodner_mstar
         mle%bodner_nstar = fcfg%bodner_nstar
         mle%min_wstar2 = fcfg%min_wstar2

         ! B5 takes the mixed-layer depth from epbl%mld — EPBL is the
         ! enabler and must be on.
         if (.not. ocean_state%epbl%enable) then
            call fail("ocean_foxkemper_nml: enable=.true. requires "// &
                      "ocean_epbl_nml enable=.true. (B5 reads epbl%mld)", ierr, OCEAN_STATUS_ERR_SETUP)
            return
         end if
         ! resolution_taper is a B2 hook; the res_fn is not available yet.
         if (mle%resolution_taper) then
            call fail("ocean_foxkemper_nml: resolution_taper=.true. "// &
                      "requires the B2 resolution function (not yet "// &
                      "available) — leave it .false. until B2 lands", ierr, OCEAN_STATUS_ERR_SETUP)
            return
         end if
         if (mle%tail_dh /= 0.0_wp) then
            call fail("ocean_foxkemper_nml: tail_dh /= 0 (mu cubic "// &
                      "tail) is deferred; use the exact mu (tail_dh=0)", ierr, OCEAN_STATUS_ERR_SETUP)
            return
         end if

         if (compute_rank == 0) then
            if (fcfg%use_bodner) then
               call logger%info("Fox-Kemper MLE:   restratification enabled  "// &
                                "Bodner-2023 frontogenesis arrest  Cr="//to_string(fcfg%cr))
            else
               call logger%info("Fox-Kemper MLE:   restratification enabled  Ce="// &
                                to_string(fcfg%ce)//"  timescale="// &
                                merge("FK11-mixrate", "bare Ce/|f| ", fcfg%use_mom_mixrate))
            end if
         end if
      end associate
      if (present(ierr)) ierr = OCEAN_STATUS_OK
   end subroutine configure_ocean_foxkemper

   subroutine configure_ocean_kappa_shear(cfg, ocean_state, grid, compute_rank, ierr)
      !! Copy the `&ocean_kappa_shear_nml` knobs onto the kappa-shear slot
      !! — every field, end to end (don't repeat the KPP dead-config
      !! gap).  Fills `f_centre` from the same beta-plane parameters the
      !! Coriolis slot uses, copies the EOS hookup, and validates.  No
      !! mutual exclusion: kappa-shear is an interior closure that
      !! coexists with KPP / EPBL and PP81/background.
      type(config_t), intent(in) :: cfg
      type(ocean_state_t), intent(inout) :: ocean_state
      type(hgrid_t), intent(in) :: grid
      integer, intent(in) :: compute_rank
      integer, intent(out), optional :: ierr
         !! Non-zero on a kappa-shear configuration conflict when present;
         !! absent behaves as today (`error stop`).

      associate (ks => ocean_state%kshear, kcfg => cfg%ocean%kshear)
         ! `enable` is flipped LAST, only once every validation below has
         ! passed (P0.1 review F9 — `configure_ocean_porous` is the
         ! pattern): setting it up front left a half-configured slot
         ! flagged enabled on any of this routine's many failure returns.
         if (.not. kcfg%enable) then
            ks%enable = .false.
            if (present(ierr)) ierr = OCEAN_STATUS_OK
            return
         end if

         ! Fail loud at configure (wavespeed precedent): both kappa-shear
         ! kernels gather into fixed-size NZ_STACK_MAX column arrays, so
         ! nz > NZ_STACK_MAX would silently overrun per-thread stack.
         if (ocean_state%multilayer%nz_ml > NZ_STACK_MAX) then
            call fail("ocean_kappa_shear_nml: nz_layers exceeds "// &
                      "NZ_STACK_MAX (raise NZ_STACK_MAX in rdb_constants)", ierr, OCEAN_STATUS_ERR_SETUP)
            return
         end if

         ! Every JHL08 knob (no dead-config gaps).
         ks%ri_crit = kcfg%ri_crit
         ks%shearmix_rate = kcfg%shearmix_rate
         ks%fri_curvature = kcfg%fri_curvature
         ks%c_n = kcfg%c_n
         ks%c_s = kcfg%c_s
         ks%lambda = kcfg%lambda
         ks%lz_rescale = kcfg%lz_rescale
         ks%kappa_0 = kcfg%kappa_0
         ks%kappa_seed = kcfg%kappa_seed
         ks%kappa_trunc = kcfg%kappa_trunc
         ks%tke_bg = kcfg%tke_bg
         ks%tol_err = kcfg%tol_err
         ks%max_inner_it = kcfg%max_inner_it
         ks%max_substep_it = kcfg%max_substep_it
         ks%src_max_chg = kcfg%src_max_chg
         ks%prandtl_turb = kcfg%prandtl_turb
         ks%vel_underflow = kcfg%vel_underflow
         ks%massless_merge = kcfg%massless_merge
         ks%vertex_geometric_mean = kcfg%vertex_geometric_mean
         ks%vertex_geomean_kdmin = kcfg%vertex_geomean_kdmin

         ! EOS hookup: the buoyancy derivatives use the SAME EOS handle
         ! the dyn-core runs (shared flat-POD copy — one source of
         ! truth, no scalar copies that can drift).
         ks%eos = ocean_state%eos
         ks%rho0 = ocean_state%eos%rho0

         ! |f| at cell centres, same scheme the Coriolis slot uses (D7).
         call fill_coriolis_centre(cfg, ocean_state%metrics, grid, ks%f_centre)

         ! Vertex form (MOM6 VERTEX_SHEAR): allocate the corner fields
         ! (deliberately not in `init` — the corner carrier is nz+1 full
         ! planes, paid only when selected) and fill the SIGNED corner
         ! f via the same generator-driven scheme (the kernel squares
         ! it).  Runs before `ocean_state_enter_data`, so the new
         ! allocatables map with the slot.
         if (kcfg%at_vertex) then
            ! nghost >= 2 fail-loud (pv_adv_required_nghost precedent):
            ! corners are solved on [2,nx]x[2,ny] only, so the outermost
            ! array ring averages with never-solved zero ring corners.
            ! With nghost >= 2 that ring is entirely ghost cells and the
            ! owned-cell answer is exact; with nghost < 2 the depressed
            ! first-ring kt could leak into owned cells via smoothing.
            if (grid%nghost < 2) then
               call fail("ocean_kappa_shear_nml: at_vertex requires "// &
                         "nghost >= 2 (corner solve covers interior "// &
                         "corners only; got nghost="//to_string(grid%nghost)//")", ierr, OCEAN_STATUS_ERR_SETUP)
               return
            end if
            call ks%init_vertex(grid, ocean_state%multilayer%nz_ml)
            call fill_coriolis_corner(cfg, ocean_state%metrics, grid, ks%f_corner)
         end if

         ! Validation.
         if (.not. cfg%ocean%vmix%use_closure) then
            call fail("ocean_kappa_shear_nml: enable=.true. requires "// &
                      "ocean_vmix_nml use_closure=.true. (kappa-shear "// &
                      "folds into the interior closure's kv/kt)", ierr, OCEAN_STATUS_ERR_SETUP)
            return
         end if
         if (.not. cfg%ocean%thermo%enable_thermodynamics) then
            call fail("ocean_kappa_shear_nml: enable=.true. requires "// &
                      "active thermodynamics (kappa-shear reads T, S)", ierr, OCEAN_STATUS_ERR_SETUP)
            return
         end if
         if (ks%ri_crit <= 0.0_wp) then
            call fail("ocean_kappa_shear_nml: ri_crit must be > 0", ierr, OCEAN_STATUS_ERR_SETUP)
            return
         end if
         if (ks%max_inner_it < 1) then
            call fail("ocean_kappa_shear_nml: max_inner_it must be >= 1", ierr, OCEAN_STATUS_ERR_SETUP)
            return
         end if
         if (ks%max_substep_it < 1) then
            call fail("ocean_kappa_shear_nml: max_substep_it must be >= 1", ierr, OCEAN_STATUS_ERR_SETUP)
            return
         end if
         if (ks%kappa_0 <= 0.0_wp) then
            call fail("ocean_kappa_shear_nml: kappa_0 must be > 0", ierr, OCEAN_STATUS_ERR_SETUP)
            return
         end if
         if (ks%tol_err <= 0.0_wp) then
            call fail("ocean_kappa_shear_nml: tol_err must be > 0", ierr, OCEAN_STATUS_ERR_SETUP)
            return
         end if
         if (ks%prandtl_turb <= 0.0_wp) then
            call fail("ocean_kappa_shear_nml: prandtl_turb must be > 0", ierr, OCEAN_STATUS_ERR_SETUP)
            return
         end if
         if (ks%vertex_geomean_kdmin < 0.0_wp) then
            call fail("ocean_kappa_shear_nml: vertex_geomean_kdmin must be >= 0", ierr, OCEAN_STATUS_ERR_SETUP)
            return
         end if
         if (compute_rank == 0) then
            ! Advisory warnings (MOM6-parity semantics, not errors).
            if (kcfg%vertex_geomean_kdmin > 0.0_wp .and. &
                .not. kcfg%vertex_geometric_mean) then
               call logger%warning("ocean_kappa_shear_nml: vertex_geomean_kdmin "// &
                                   "is inert without vertex_geometric_mean")
            end if
            if (kcfg%at_vertex .and. kcfg%vertex_geometric_mean .and. &
                kcfg%vertex_geomean_kdmin == 0.0_wp) then
               call logger%warning("ocean_kappa_shear_nml: geometric mean with "// &
                                   "kdmin=0 hard-zeros Kd wherever ANY corner is 0 "// &
                                   "(every shear-zone edge); production configs "// &
                                   "use vertex_geomean_kdmin=1e-9")
            end if
         end if

         if (compute_rank == 0) then
            call logger%info("Kappa-shear:      interior closure enabled "// &
                             "(ri_crit="//to_string(kcfg%ri_crit)// &
                             "  prandtl_turb="//to_string(kcfg%prandtl_turb)// &
                             trim(merge("  at_vertex", "           ", kcfg%at_vertex))//")")
         end if
         ks%enable = .true.
      end associate
      if (present(ierr)) ierr = OCEAN_STATUS_OK
   end subroutine configure_ocean_kappa_shear

   subroutine configure_ocean_tidal_mixing(cfg, ocean_state, compute_rank, ierr)
      !! Copy the `&ocean_tidal_mixing_nml` knobs onto the tidal-mixing
      !! slot — every field, end to end (no dead-config gaps).  Seeds the
      !! prescribed bottom energy field `e_in` (v1 uniform path), copies
      !! the shared EOS hookup for the N^2 buoyancy derivatives, and
      !! validates.  No mutual exclusion: tidal mixing is an interior
      !! closure that coexists with KPP / EPBL / PP81 / kappa-shear.
      type(config_t), intent(in) :: cfg
      type(ocean_state_t), intent(inout) :: ocean_state
      integer, intent(in) :: compute_rank
      integer, intent(out), optional :: ierr
         !! Non-zero on a tidal-mixing configuration conflict when present;
         !! absent behaves as today (`error stop`).

      associate (tm => ocean_state%vmix_tidal, tcfg => cfg%ocean%tidal_mixing)
         ! `enable` is flipped LAST, only once every validation below has
         ! passed (P0.1 review F9 — `configure_ocean_porous` is the
         ! pattern): setting it up front left a half-configured slot
         ! flagged enabled on any of this routine's failure returns.
         if (.not. tcfg%enable) then
            tm%enable = .false.
            if (present(ierr)) ierr = OCEAN_STATUS_OK
            return
         end if

         ! Every St-Laurent/Simmons knob (no dead-config gaps).
         tm%gamma = tcfg%gamma
         tm%mu = tcfg%mu
         tm%zeta = tcfg%zeta
         tm%kd_max = tcfg%kd_max
         tm%prandtl_tidal = tcfg%prandtl_tidal
         tm%min_zbot = tcfg%min_zbot
         tm%e_uniform = tcfg%e_uniform
         tm%e_compute = tcfg%e_compute
         tm%kappa_itides = tcfg%kappa_itides
         tm%kappa_h2 = tcfg%kappa_h2
         tm%utide = tcfg%utide
         tm%h2_rough = tcfg%h2_rough
         tm%frac_rough = tcfg%frac_rough
         tm%e_max = tcfg%e_max

         ! EOS hookup: the buoyancy derivatives use the SAME EOS handle
         ! the dyn-core runs (shared flat-POD copy — one source of truth).
         tm%eos = ocean_state%eos
         tm%rho0 = ocean_state%eos%rho0

         ! Seed the prescribed bottom energy field (v1 uniform path).
         call tm%set_e_uniform(tm%e_uniform)

         ! Validation.
         if (.not. cfg%ocean%vmix%use_closure) then
            call fail("ocean_tidal_mixing_nml: enable=.true. requires "// &
                      "ocean_vmix_nml use_closure=.true. (tidal mixing "// &
                      "folds into the interior closure's kv/kt)", ierr, OCEAN_STATUS_ERR_SETUP)
            return
         end if
         if (.not. cfg%ocean%thermo%enable_thermodynamics) then
            call fail("ocean_tidal_mixing_nml: enable=.true. requires "// &
                      "active thermodynamics (tidal mixing reads T, S for N^2)", ierr, OCEAN_STATUS_ERR_SETUP)
            return
         end if
         if (tm%zeta <= 0.0_wp) then
            call fail("ocean_tidal_mixing_nml: zeta must be > 0", ierr, OCEAN_STATUS_ERR_SETUP)
            return
         end if
         if (tm%prandtl_tidal <= 0.0_wp) then
            call fail("ocean_tidal_mixing_nml: prandtl_tidal must be > 0", ierr, OCEAN_STATUS_ERR_SETUP)
            return
         end if

         if (compute_rank == 0) then
            call logger%info("Tidal mixing:     interior closure enabled "// &
                             "(zeta="//to_string(tcfg%zeta)//" m  "// &
                             "E="//to_string(tcfg%e_uniform)//" W/m^2)")
         end if
         tm%enable = .true.
      end associate
      if (present(ierr)) ierr = OCEAN_STATUS_OK
   end subroutine configure_ocean_tidal_mixing

   subroutine configure_ocean_conv(cfg, ocean_state, compute_rank, ierr)
      !! Copy the `&ocean_conv_nml` knobs onto `ocean_state%vmix` -- every
      !! field, no dead-config gaps.  Convective adjustment adds no new
      !! slot / allocatable (it lives on the already-unconditionally-
      !! allocated `vmix`), so unlike `configure_ocean_tidal_mixing` there
      !! is no separate `enable` latch to set on a distinct sub-object;
      !! `vmix%conv_enable` IS the latch.  No mutual exclusion with
      !! KPP / EPBL: convection masks against whichever BL depth is live
      !! this stage (`rdb_ocean_dyn.F90 vmix_apply_in_stage`).
      type(config_t), intent(in) :: cfg
      type(ocean_state_t), intent(inout) :: ocean_state
      integer, intent(in) :: compute_rank
      integer, intent(out), optional :: ierr
         !! Non-zero on a convective-adjustment configuration conflict when
         !! present; absent behaves as today (`error stop`).

      associate (vmix => ocean_state%vmix, ccfg => cfg%ocean%conv)
         ! `conv_enable` is flipped LAST, only once every validation below
         ! has passed (P0.1 review F9 — `configure_ocean_porous` is the
         ! pattern): setting it up front left a half-configured slot
         ! flagged enabled on any of this routine's failure returns.
         if (.not. ccfg%enable) then
            vmix%conv_enable = .false.
            if (present(ierr)) ierr = OCEAN_STATUS_OK
            return
         end if

         vmix%conv_kd = ccfg%kd_conv
         vmix%conv_prandtl = ccfg%prandtl_conv
         vmix%conv_n2_thresh = ccfg%n2_thresh

         ! Validation.
         if (.not. cfg%ocean%vmix%use_closure) then
            call fail("ocean_conv_nml: enable=.true. requires "// &
                      "ocean_vmix_nml use_closure=.true. (convection "// &
                      "folds into the interior closure's kv/kt)", ierr, OCEAN_STATUS_ERR_SETUP)
            return
         end if
         if (.not. cfg%ocean%thermo%enable_thermodynamics) then
            call fail("ocean_conv_nml: enable=.true. requires "// &
                      "active thermodynamics (convection reads rho_layer, "// &
                      "which is only refreshed by the EOS)", ierr, OCEAN_STATUS_ERR_SETUP)
            return
         end if
         if (ccfg%kd_conv < 0.0_wp) then
            call fail("ocean_conv_nml: kd_conv must be >= 0", ierr, OCEAN_STATUS_ERR_SETUP)
            return
         end if
         if (ccfg%prandtl_conv <= 0.0_wp) then
            call fail("ocean_conv_nml: prandtl_conv must be > 0", ierr, OCEAN_STATUS_ERR_SETUP)
            return
         end if

         if (compute_rank == 0) then
            call logger%info("Convection:       interior closure enabled "// &
                             "(kd_conv="//to_string(ccfg%kd_conv)//" m^2/s  "// &
                             "n2_thresh="//to_string(ccfg%n2_thresh)//" s^-2)")
         end if
         vmix%conv_enable = .true.
      end associate
      if (present(ierr)) ierr = OCEAN_STATUS_OK
   end subroutine configure_ocean_conv

   subroutine configure_ocean_ddiff(cfg, ocean_state, compute_rank, ierr)
      !! Copy the `&ocean_ddiff_nml` knobs onto `ocean_state%vmix` -- every
      !! field, no dead-config gaps.  Like convection, double diffusion adds
      !! no new slot (it rides the unconditionally-allocated `vmix` and the
      !! already-mirrored `vmix%eos`); `vmix%ddiff_enable` IS the latch.
      !! Folded into `vmix_split_kd_heat_salt`, so it needs the interior
      !! closure pipeline (`use_closure`) and thermodynamics (it reads T/S
      !! and the EOS alpha/beta).  `enable=.false.` => bit-identical.
      type(config_t), intent(in) :: cfg
      type(ocean_state_t), intent(inout) :: ocean_state
      integer, intent(in) :: compute_rank
      integer, intent(out), optional :: ierr
         !! Non-zero on a double-diffusion configuration conflict when
         !! present; absent behaves as today (`error stop`).

      associate (vmix => ocean_state%vmix, dcfg => cfg%ocean%ddiff)
         ! `ddiff_enable` is flipped LAST, only once every validation
         ! below has passed (P0.1 review F9 — `configure_ocean_porous` is
         ! the pattern): setting it up front left a half-configured slot
         ! flagged enabled on any of this routine's failure returns.
         if (.not. dcfg%enable) then
            vmix%ddiff_enable = .false.
            if (present(ierr)) ierr = OCEAN_STATUS_OK
            return
         end if

         vmix%ddiff_strat_param_max = dcfg%strat_param_max
         vmix%ddiff_kappa_s = dcfg%kappa_ddiff_s
         vmix%ddiff_exp1 = dcfg%ddiff_exp1
         vmix%ddiff_exp2 = dcfg%ddiff_exp2
         vmix%ddiff_param1 = dcfg%param1
         vmix%ddiff_param2 = dcfg%param2
         vmix%ddiff_param3 = dcfg%param3
         vmix%ddiff_mol_diff = dcfg%mol_diff
         vmix%ddiff_use_k90 = dcfg%use_k90

         ! Validation.
         if (.not. cfg%ocean%vmix%use_closure) then
            call fail("ocean_ddiff_nml: enable=.true. requires "// &
                      "ocean_vmix_nml use_closure=.true. (double diffusion "// &
                      "folds into the interior closure's heat/salt split)", ierr, OCEAN_STATUS_ERR_SETUP)
            return
         end if
         if (.not. cfg%ocean%thermo%enable_thermodynamics) then
            call fail("ocean_ddiff_nml: enable=.true. requires "// &
                      "active thermodynamics (double diffusion reads T/S "// &
                      "and the EOS alpha/beta)", ierr, OCEAN_STATUS_ERR_SETUP)
            return
         end if
         if (dcfg%strat_param_max <= 1.0_wp) then
            call fail("ocean_ddiff_nml: strat_param_max must be > 1 "// &
                      "(the fingering form divides by strat_param_max - 1)", ierr, OCEAN_STATUS_ERR_SETUP)
            return
         end if
         if (dcfg%kappa_ddiff_s < 0.0_wp .or. dcfg%mol_diff < 0.0_wp) then
            call fail("ocean_ddiff_nml: kappa_ddiff_s and mol_diff must be >= 0", ierr, OCEAN_STATUS_ERR_SETUP)
            return
         end if

         if (compute_rank == 0) then
            call logger%info("Double diffusion: enabled (Rrho_max="// &
                             to_string(dcfg%strat_param_max)//"  K_f="// &
                             to_string(dcfg%kappa_ddiff_s)//" m^2/s  "// &
                             "conv="//trim(merge("K90 ", "MC76", dcfg%use_k90))//")")
         end if
         vmix%ddiff_enable = .true.
      end associate
      if (present(ierr)) ierr = OCEAN_STATUS_OK
   end subroutine configure_ocean_ddiff

   subroutine configure_ocean_tides(cfg, ocean_state, grid, compute_rank, ierr)
      !! Configure the equilibrium body-force tide slot (C1): parse the
      !! constituent list + reference dates, fill the astronomy catalog
      !! (phase0, nodal f/u), and build the (nx,ny,3) spatial-structure
      !! arrays from `metrics%geolatT/geolonT`.  Host-side setup — runs
      !! AFTER `configure_ocean_metrics` (lat/lon must be filled) and
      !! BEFORE `ocean_state_enter_data`.  `enable=.false.` => no-op,
      !! bit-identical.
      type(config_t), intent(in) :: cfg
      type(ocean_state_t), intent(inout) :: ocean_state
      type(hgrid_t), intent(in) :: grid
      integer, intent(in) :: compute_rank
      integer, intent(out), optional :: ierr
         !! Non-zero on a tides configuration conflict when present;
         !! absent behaves as today (`error stop`).
      integer :: cat_idx(TIDES_CATALOG_SIZE)
      integer :: nconst, y, m, d, nx, ny, local_ierr
      real(wp) :: dref, dnodal
      logical :: ok
      character(len=16) :: nodal_str

      associate (td => ocean_state%tides, tcfg => cfg%ocean%tides)
         td%enable = tcfg%enable
         if (.not. tcfg%enable) then
            if (present(ierr)) ierr = OCEAN_STATUS_OK
            return
         end if

         td%use_sal = tcfg%use_sal
         td%beta_sal = tcfg%beta_sal
         td%t_epoch = 0.0_wp

         if (td%use_sal .and. td%beta_sal <= 0.0_wp .and. compute_rank == 0) then
            call logger%warning("ocean_tides_nml: use_sal=.true. but "// &
                                "beta_sal<=0 => scalar SAL is inert "// &
                                "(typical beta ~0.085-0.12)")
         end if

         ! Parse the active constituent list -> catalog indices.
         if (present(ierr)) then
            call parse_constituent_list(tcfg%constituents, cat_idx, nconst, ierr=local_ierr)
            if (local_ierr /= 0) then
               ierr = local_ierr
               return
            end if
         else
            call parse_constituent_list(tcfg%constituents, cat_idx, nconst)
         end if
         if (nconst < 1) then
            call fail("ocean_tides_nml: constituents='"// &
                      trim(tcfg%constituents)//"' has no valid entries", ierr, OCEAN_STATUS_ERR_SETUP)
            return
         end if

         ! Reference date -> day number since 1900-01-01.
         call parse_date_string(tcfg%ref_date, y, m, d, ok)
         if (.not. ok) then
            call fail("ocean_tides_nml: bad ref_date '"// &
                      trim(tcfg%ref_date)//"' (want YYYY-MM-DD)", ierr, OCEAN_STATUS_ERR_SETUP)
            return
         end if
         dref = days_since_1900(y, m, d)

         nodal_str = tcfg%nodal_ref_date
         if (len_trim(nodal_str) == 0) nodal_str = tcfg%ref_date
         call parse_date_string(nodal_str, y, m, d, ok)
         if (.not. ok) then
            call fail("ocean_tides_nml: bad nodal_ref_date '"// &
                      trim(nodal_str)//"' (want YYYY-MM-DD)", ierr, OCEAN_STATUS_ERR_SETUP)
            return
         end if
         dnodal = days_since_1900(y, m, d)

         nx = grid%nx_total
         ny = grid%ny_total
         call tides_configure_astronomy(td, cat_idx(1:nconst), nconst, &
                                        dref, dnodal, tcfg%add_nodal, nx, ny)
         call tides_build_struct(td, ocean_state%metrics%geolatT, &
                                 ocean_state%metrics%geolonT, nx, ny)

         if (compute_rank == 0) then
            call logger%info("Equilibrium tide: C1 body force enabled ("// &
                             to_string(nconst)//" constituents, ref="// &
                             trim(tcfg%ref_date)//")")
         end if
      end associate
      if (present(ierr)) ierr = OCEAN_STATUS_OK
   end subroutine configure_ocean_tides

   subroutine configure_ocean_p_surf(cfg, ocean_state, grid, compute_rank)
      !! Configure the atmospheric surface-pressure loading slot (PR-17):
      !! copy `enable`, take ρ₀ from `ocean_state%eos%rho0` (the single ρ₀
      !! of record — NOT a namelist knob), and allocate the seam fields.
      !! Host-side setup — runs BEFORE `ocean_state_enter_data`.
      !! `enable=.false.` => no-op, bit-identical.  The uniform-`p_surf`
      !! inert warning is emitted in `validate_config`; here we only log the
      !! enable on rank 0.
      type(config_t), intent(in) :: cfg
      type(ocean_state_t), intent(inout) :: ocean_state
      type(hgrid_t), intent(in) :: grid
      integer, intent(in) :: compute_rank
      integer :: nx, ny

      associate (ps => ocean_state%p_surf, pcfg => cfg%ocean%psurf)
         ps%enable = pcfg%enable
         if (.not. pcfg%enable) return

         ! Single ρ₀ of record: the model's own reference density, so the
         ! inverse barometer scales with whatever `&ocean_ic_nml rho_0`
         ! set.  `eos%rho0` is assigned in `ocean_state_init_from_config`
         ! (runs earlier), so it is finalised here.
         ps%rho0 = ocean_state%eos%rho0

         nx = grid%nx_total
         ny = grid%ny_total
         call p_surf_configure(ps, nx, ny)

         if (compute_rank == 0) then
            call logger%info("Surface pressure: inverse-barometer PGF enabled "// &
                             "(eta_ib = -p_surf/(rho0*g_bt), rho0 = "// &
                             to_string(ps%rho0)//" kg/m^3)")
         end if
      end associate
   end subroutine configure_ocean_p_surf

   subroutine configure_ocean_wave_drag(cfg, ocean_state, grid, compute_rank, ierr)
      !! Configure the barotropic linear (Rayleigh) wave-drag piston-velocity
      !! maps (Egbert & Ray 2001; Jayne & St Laurent 2001) — the bulk energy
      !! sink for the barotropic tide, MOM6 `BT_LINEAR_WAVE_DRAG`. Builds a
      !! host-only h-point `r_h(nx,ny)` map (uniform scalar or a
      !! resolved-bathymetry roughness proxy), scales it, averages h->face
      !! into `bt_work%lwd_drag_u/v`, and leaves the arrays unallocated when
      !! the knob is off (bit-identical). MUST run AFTER bathymetry is set
      !! (`ocean_state%barotropic%b`) and land masking
      !! (`configure_ocean_land_mask`) and BEFORE `ocean_state_enter_data` —
      !! the `!$acc enter data copyin` in `barotropic_workstate_enter_data`
      !! carries these host-filled values to the device (CLAUDE.md gotcha
      !! (2): arrays mapped `create` do not carry pre-map host values, so
      !! this ordering is load-bearing).
      type(config_t), intent(in) :: cfg
      type(ocean_state_t), intent(inout) :: ocean_state
      type(hgrid_t), intent(in) :: grid
      integer, intent(in) :: compute_rank
      integer, intent(out), optional :: ierr
         !! Non-zero on an unreachable/unimplemented `wave_drag_form` when
         !! present; absent behaves as today (`error stop`).
      real(wp), allocatable :: r_h(:, :)
      integer :: i, j, nx, ny

      associate (bw => ocean_state%dyn%bt_work, bcfg => cfg%ocean%bt)
         bw%lwd_enable = bcfg%wave_drag
         if (.not. bcfg%wave_drag) then
            if (present(ierr)) ierr = OCEAN_STATUS_OK
            return
         end if

         nx = grid%nx_total
         ny = grid%ny_total
         allocate (bw%lwd_drag_u(nx + 1, ny), source=0.0_wp)
         allocate (bw%lwd_drag_v(nx, ny + 1), source=0.0_wp)
         allocate (r_h(nx, ny), source=0.0_wp)

         select case (trim(bcfg%wave_drag_form))
         case ("uniform")
            r_h = bcfg%wave_drag_r_uniform
         case ("roughness_proxy")
            call wave_drag_roughness_proxy(ocean_state%barotropic%b, &
                                           ocean_state%metrics%wet_T, &
                                           nx, ny, grid%nghost, bcfg%wave_drag_kappa, &
                                           bcfg%wave_drag_n_bot, bcfg%wave_drag_h2_max, r_h)
         case default
            ! Unreachable: validate_config fails loud on any other tag
            ! (including "file", registered but not yet implemented).
            call fail("configure_ocean_wave_drag: unreachable wave_drag_form='"// &
                      trim(bcfg%wave_drag_form)//"'", ierr, OCEAN_STATUS_ERR_SETUP)
            return
         end select

         r_h = bcfg%wave_drag_scale*r_h

         ! h -> face average (as MOM6 forms its wave-drag face map), built ONCE
         ! here; the array-edge faces (i=1/nx+1, j=1/ny+1) stay at their
         ! source=0.0 init value.
         do i = 2, nx
            do j = 1, ny
               bw%lwd_drag_u(i, j) = 0.5_wp*(r_h(i - 1, j) + r_h(i, j))
            end do
         end do
         do j = 2, ny
            do i = 1, nx
               bw%lwd_drag_v(i, j) = 0.5_wp*(r_h(i, j - 1) + r_h(i, j))
            end do
         end do

         if (compute_rank == 0) then
            call logger%info("Barotropic wave drag: ON  form="//trim(bcfg%wave_drag_form)// &
                             "  scale="//to_string(bcfg%wave_drag_scale)// &
                             "  max(r_H)="//to_string(maxval(bw%lwd_drag_u))// &
                             " m/s  mean(r_H)="// &
                             to_string(sum(bw%lwd_drag_u)/real(size(bw%lwd_drag_u), wp))//" m/s")
         end if
      end associate
      if (present(ierr)) ierr = OCEAN_STATUS_OK
   end subroutine configure_ocean_wave_drag

   subroutine configure_ocean_z_fixed_profile(cfg, ocean_state, compute_rank, log_it)
      !! Resolve the `VCOORD_Z_FIXED` nominal layering onto the vcoord slot —
      !! and the `VCOORD_ZSTAR` one, which is the same nominal profile
      !! (MOM6 z* dilates it per column; `ocean_vcoord_zstar_target`):
      !! `z_fixed_h_ref = &ocean_topo_nml max_depth` (the uniform
      !! `max_depth/nz` spacing — the default, byte-identical), or, under
      !! `&vcoord_nml z_fixed_profile = "list" | "tanh"`, the stretched
      !! per-layer tables `z_fixed_zi` / `z_fixed_dz` built by
      !! `rdb_vcoord :: z_fixed_nominal_dz` (`z_fixed_h_ref` then becomes
      !! the profile's total depth).
      !!
      !! The same tables are the z* NOMINAL FLOOR of `VCOORD_HYCOM` (MOM6
      !! HYCOM1 floors its interfaces at the z* `coordinateResolution` its
      !! ALE_COORDINATE_CONFIG defines), so this also runs for `hycom`.
      !!
      !! Idempotent (it rebuilds from `cfg` each call).  Called twice: by
      !! `engine_setup` BEFORE the IC seed — the cavity `z_fixed` seed lays
      !! `h_layer` from the same target builder and must see the same
      !! profile — and from `configure_ocean_lateral`, which has always
      !! owned `z_fixed_h_ref`.  Both precede `ocean_state_enter_data`, so
      !! the copyin captures the tables.  `validate_config` has already
      !! refused a profile that does not build, or one on another family.
      type(config_t), intent(in) :: cfg
      type(ocean_state_t), intent(inout) :: ocean_state
      integer, intent(in) :: compute_rank
      logical, intent(in) :: log_it
         !! Log the resolved profile (rank 0).
      integer :: code, ierr, nz
      real(wp), allocatable :: dz(:)

      ocean_state%vcoord%z_fixed_h_ref = cfg%ocean%topo%max_depth
      ocean_state%vcoord%z_fixed_use_profile = .false.
      if (.not. ocean_state%vcoord%is_init) return
      ! Three readers of the z* nominal resolution: `z_fixed` (its levels),
      ! `zstar` (MOM6 z*, its levels) and `hycom` (its z* nominal floor,
      ! MOM6 HYCOM1 coordinateResolution).
      if (ocean_state%vcoord%coord_type /= VCOORD_Z_FIXED .and. &
          ocean_state%vcoord%coord_type /= VCOORD_ZSTAR .and. &
          ocean_state%vcoord%coord_type /= VCOORD_HYCOM) return
      code = parse_z_fixed_profile(cfg%z_fixed_profile)
      if (code == ZFIXED_PROFILE_UNIFORM .or. code == ZFIXED_PROFILE_INVALID) return
      nz = ocean_state%vcoord%nz_ml
      allocate (dz(nz))
      call z_fixed_nominal_dz(code, nz, cfg%ocean%topo%max_depth, cfg%z_fixed_dz, &
                              cfg%z_fixed_dz_top, cfg%z_fixed_tanh_center, &
                              cfg%z_fixed_tanh_width, dz, ierr)
      if (ierr /= ZFIXED_DZ_OK) return
      call ocean_vcoord_set_z_fixed_profile(ocean_state%vcoord, dz)
      if (log_it .and. compute_rank == 0) then
         call logger%info(trim(cfg%vcoord_type)//" z* profile:  "//trim(cfg%z_fixed_profile)// &
                          " — nominal dz "//to_string(dz(1))//" m (surface) … "// &
                          to_string(dz(nz))//" m (bed), total "// &
                          to_string(ocean_state%vcoord%z_fixed_h_ref)//" m over "// &
                          to_string(nz)//" layers")
      end if
   end subroutine configure_ocean_z_fixed_profile

   subroutine configure_ocean_k_top(cfg, ocean_state, grid, compute_rank)
      !! Fill `ms%k_top` / `k_top_u` / `k_top_v` — the shared index of
      !! the first LIVE layer counting down from the top, and the field
      !! every top-side consumer reads instead of spelling `nz`.
      !!
      !! Under a quasi-geopotential coordinate beneath an ice shelf
      !! (`vcoord_type = "z_fixed"` with `vcoord%z_top > 0`) the layers
      !! whose nominal geopotential range lies INSIDE the ice are inert
      !! fillers at `zstar_h_min`, so on an ice-covered column
      !! `k = nz` is NOT the ice-adjacent layer.  This is the only
      !! producer of that index.
      !!
      !! **Static.** The pattern is read ONCE, from
      !! `ocean_vcoord_z_fixed_target` at `eta = 0` — the same kernel,
      !! the same `bt_H_ref` and the same `z_top` the closed-face mask
      !! is built from, so there is exactly ONE definition of "live" in
      !! the tree and `test_ocean_ktop` can assert the two agree.  Under
      !! `z_fixed` `eta` is absorbed by the first live layer (the partial
      !! top cell) and a filler's target is `zstar_h_min` whatever `eta`
      !! does, so the live/filler pattern does not move and there is
      !! nothing to recompute per stage.
      !!
      !! **Every other coordinate is a literal no-op**: the arrays were
      !! allocated at `source = nz_ml` in `multilayer_state_init`, which
      !! IS the answer wherever nothing vanishes against the top, so the
      !! whole phase is bit-identical off `z_fixed`.
      !!
      !! **Ordering.** Same as `configure_ocean_closed_faces` — AFTER
      !! `configure_ocean_cavity` (`vcoord%z_top`), AFTER
      !! `configure_ocean_bt_split` (`bt_H_ref`), AFTER the periodic-wrap
      !! / halo pass, and BEFORE `ocean_state_enter_data` (the host fill
      !! is what the `copyin` captures).
      type(config_t), intent(in) :: cfg
      type(ocean_state_t), intent(inout) :: ocean_state
      type(hgrid_t), intent(in) :: grid
      integer, intent(in) :: compute_rank

      integer :: nx, ny, nz, n_filler
      real(wp) :: h_nominal, h_min
      real(wp), allocatable :: tgt(:, :, :), eta0(:, :)

      if (.not. ocean_state%multilayer%is_init) return
      if (ocean_state%vcoord%coord_type /= VCOORD_Z_FIXED) return
      if (ocean_state%vcoord%z_fixed_h_ref <= 0.0_wp) return
      if (.not. ocean_state%dyn%bt_work%is_init) return
      if (.not. cfg%ocean%cavity_dyn%enable) return

      nx = grid%nx_total
      ny = grid%ny_total
      nz = ocean_state%multilayer%nz_ml
      h_nominal = ocean_state%vcoord%z_fixed_h_ref/real(nz, wp)
      h_min = ocean_state%vcoord%zstar_h_min

      allocate (tgt(nx, ny, nz), source=0.0_wp)
      allocate (eta0(nx, ny), source=0.0_wp)
      call ocean_vcoord_z_fixed_target(tgt, ocean_state%dyn%bt_work%bt_H_ref, &
                                       eta0, ocean_state%vcoord%z_top, &
                                       nx, ny, nz, h_nominal, &
                                       ocean_state%vcoord%z_fixed_use_profile, &
                                       ocean_state%vcoord%z_fixed_zi, &
                                       ocean_state%vcoord%z_fixed_dz, h_min)
      call ocean_vcoord_k_top_from_target(ocean_state%multilayer%k_top, &
                                          ocean_state%multilayer%k_top_u, &
                                          ocean_state%multilayer%k_top_v, &
                                          tgt, nx, ny, nz, H_VANISHED)
      n_filler = count(ocean_state%multilayer%k_top < nz)
      deallocate (tgt, eta0)

      if (compute_rank == 0) then
         call logger%info("z_fixed k_top: "//to_string(n_filler)//"/"// &
                          to_string(nx*ny)//" columns carry top-side "// &
                          "fillers (k_top < nz); min k_top = "// &
                          to_string(minval(ocean_state%multilayer%k_top))// &
                          ", nz = "//to_string(nz))
      end if
   end subroutine configure_ocean_k_top

   subroutine configure_ocean_k_bot(ocean_state, grid, compute_rank)
      !! Fill `ms%k_bot` / `k_bot_u` / `k_bot_v` — the shared index of the
      !! first LIVE layer counting UP from the bed, and the field every
      !! bed-side consumer reads instead of spelling `1`.  The bed-side
      !! mirror of `configure_ocean_k_top`.
      !!
      !! Under `vcoord_type = "z_fixed"` (with a resolved `z_fixed_h_ref`)
      !! every column shallower than the nominal stack carries inert
      !! FILLERS at `zstar_h_min` below its partial bed cell, so `k = 1`
      !! is not the bed-adjacent layer: a bottom drag, an implicit-drag
      !! diagonal, a geothermal deposit or a tidal-mixing bed anchor put
      !! on `k = 1` lands on a layer the vdiff solve has cut out of the
      !! column.  Unlike `k_top` this is NOT gated on a cavity — bed
      !! fillers exist on every `z_fixed` run with topography.
      !!
      !! **Static.** Read ONCE from `ocean_vcoord_z_fixed_target` at
      !! `eta = 0` — the same kernel, `bt_H_ref`, `z_top` and profile the
      !! closed-face mask and `k_top` are built from (one definition of
      !! "live").  The bed is static and `eta` is absorbed by the first
      !! live layer at the TOP, so the bed-side pattern never moves.  It
      !! is derived from bathymetry + the vcoord config, so it is rebuilt
      !! on every start (cold or warm) and is not restart state.
      !!
      !! **Every other coordinate is a literal no-op**: the arrays were
      !! allocated at `source = 1` in `multilayer_state_init`, which IS
      !! the answer wherever nothing vanishes against the bed.
      !!
      !! **Seams.** The centre index is built from the ghost-filled
      !! `bt_H_ref`, so it is seam-correct by construction; the OUTERMOST
      !! face of each face twin cannot be evaluated locally (it needs a
      !! cell beyond the array) and takes its one column.  A plain face
      !! halo exchange (integer → real → integer: the halo layer moves
      !! `real(wp)`, and an integer < 2**53 round-trips exactly) makes
      !! that ghost face carry its owner's value on a tile seam or a
      !! periodic wrap.  Single-rank non-periodic: a no-op.
      !!
      !! **Ordering.** Same as `configure_ocean_k_top`: after the cavity
      !! draft, after `configure_ocean_bt_split` (`bt_H_ref`), after the
      !! periodic-wrap / halo pass, BEFORE `ocean_state_enter_data` (the
      !! host fill is what the `copyin` captures).
      type(ocean_state_t), intent(inout) :: ocean_state
      type(hgrid_t), intent(in) :: grid
      integer, intent(in) :: compute_rank

      integer :: nx, ny, nz, n_filler
      real(wp) :: h_nominal, h_min
      real(wp), allocatable :: tgt(:, :, :), eta0(:, :), fx(:, :), fy(:, :)

      if (.not. ocean_state%multilayer%is_init) return
      if (ocean_state%vcoord%coord_type /= VCOORD_Z_FIXED) return
      if (ocean_state%vcoord%z_fixed_h_ref <= 0.0_wp) return
      if (.not. ocean_state%dyn%bt_work%is_init) return
      if (maxval(ocean_state%dyn%bt_work%bt_H_ref) <= 0.0_wp) return

      nx = grid%nx_total
      ny = grid%ny_total
      nz = ocean_state%multilayer%nz_ml
      h_nominal = ocean_state%vcoord%z_fixed_h_ref/real(nz, wp)
      h_min = ocean_state%vcoord%zstar_h_min

      allocate (tgt(nx, ny, nz), source=0.0_wp)
      allocate (eta0(nx, ny), source=0.0_wp)
      call ocean_vcoord_z_fixed_target(tgt, ocean_state%dyn%bt_work%bt_H_ref, &
                                       eta0, ocean_state%vcoord%z_top, &
                                       nx, ny, nz, h_nominal, &
                                       ocean_state%vcoord%z_fixed_use_profile, &
                                       ocean_state%vcoord%z_fixed_zi, &
                                       ocean_state%vcoord%z_fixed_dz, h_min)
      call ocean_vcoord_k_bot_from_target(ocean_state%multilayer%k_bot, &
                                          ocean_state%multilayer%k_bot_u, &
                                          ocean_state%multilayer%k_bot_v, &
                                          tgt, nx, ny, nz, H_VANISHED)
      deallocate (tgt, eta0)

      if (ocean_halo_is_init()) then
         allocate (fx(nx + 1, ny), fy(nx, ny + 1))
         fx = real(ocean_state%multilayer%k_bot_u, wp)
         fy = real(ocean_state%multilayer%k_bot_v, wp)
         call ocean_halo_face_x(fx, device_resident=.false.)
         call ocean_halo_face_y(fy, device_resident=.false.)
         ocean_state%multilayer%k_bot_u = nint(fx)
         ocean_state%multilayer%k_bot_v = nint(fy)
         deallocate (fx, fy)
      end if

      n_filler = count(ocean_state%multilayer%k_bot > 1)
      if (compute_rank == 0) then
         call logger%info("z_fixed k_bot: "//to_string(n_filler)//"/"// &
                          to_string(nx*ny)//" columns carry bed-side "// &
                          "fillers (k_bot > 1); max k_bot = "// &
                          to_string(maxval(ocean_state%multilayer%k_bot))// &
                          ", nz = "//to_string(nz))
      end if
   end subroutine configure_ocean_k_bot

   subroutine configure_ocean_closed_faces(cfg, ocean_state, grid, compute_rank, ierr)
      !! Build the static partial-step z-level FACE-CLOSURE mask
      !! (`&vcoord_nml zfixed_closed_faces`; Adcroft, Hill & Marshall
      !! 1997; Losch 2008 §2.1 for the ice-shelf cavity).
      !!
      !! Under `vcoord_type = "z_fixed"` a layer whose nominal
      !! geopotential range lies inside the bed — or inside the ice draft
      !! — carries an inert FILLER of thickness `zstar_h_min`
      !! (`<= H_VANISHED`).  A velocity face at which layer `k` is a
      !! filler on EITHER side stays OPEN today, and the FV pressure
      !! gradient evaluated across that staircase step drives
      !! `a = |ρ′|·g·Δz_step/(ρ₀·dx)` out of a resting stratified state —
      !! independent of the filler thickness, so no `h`-gate can reach
      !! it.  A z-LEVEL model treats such a face as a WALL for that
      !! layer: no normal velocity, no mass or tracer flux, free-slip.
      !!
      !! This fills `metrics%open_u/open_v` with that wall, ONCE, from
      !! the coordinate's target at `η = 0` (`ocean_vcoord_eta0_target`)
      !! — the same kernel the ALE regrid and the IC seed use, so there
      !! is no second definition of "live".  The mask is STATIC: the bed
      !! and the draft are static, and under `z_fixed` `η` is absorbed by
      !! the first LIVE layer (the partial cell), so the live/filler
      !! pattern does not move to first order in `η/h_partial`.
      !!
      !! **`zstar_full` too.**  `build_zref_full` lays a z-level fine
      !! zone (`h_surf` layers from the surface) over a terrain-following
      !! coarse zone; a column shallower than the fine zone ends in a
      !! partial cell and every layer below it is a `zstar_h_min` filler —
      !! the same staircase as `z_fixed`'s bed, with the same
      !! thickness-independent FV-PGF defect across it.  Its target puts
      !! `η ≥ 0` in the surface layer (pattern EXACTLY static) and clips
      !! `η < 0` from the bed (a bed-most live layer thinner than `|η|`
      !! flips — fewer columns than `z_fixed` flips at the same `|η|` on
      !! the 1-degree Southern Ocean).  So the mask is built from the
      !! `ZSTAR_FULL` target exactly as it is from the `z_fixed` one, and
      !! every consumer is reused unchanged.  It closes FILLER faces
      !! only; the terrain-following coarse zone's open faces keep the
      !! sigma PGF error.
      !!
      !! **`zstar` (MOM6 z*).**  `ocean_vcoord_zstar_target` dilates the
      !! `z_fixed` nominal profile by the column's free-surface stretching
      !! and decides every layer's liveness at `η = 0`, so its pattern is
      !! EXACTLY static for `η` of either sign (MOM6 `build_zstar_column`:
      !! the dilation keeps the ratios) and its `η = 0` target is the
      !! `z_fixed` one bit for bit — the same mask, every consumer reused.
      !!
      !! It also seeds the barotropic face widths `dy_cu_bt`/`dx_cv_bt`
      !! with the OPEN-depth fraction of the face, so the barotropic
      !! solve is not blind to the closed layers.  That seed is refreshed
      !! from the LIVE `h` every outer step by `ocean_porous_refresh`;
      !! this is only the `η = 0` value the first stage reads.
      !!
      !! **Ordering.** MUST run AFTER `configure_ocean_cavity` (which
      !! fills `vcoord%z_top`), after `configure_ocean_bt_split` (which
      !! lays `bt_H_ref`) and after `configure_ocean_land_mask` and the
      !! periodic-wrap / halo pass (the target is built from GHOST-FILLED
      !! `bt_H_ref` and `z_top`, which is what makes the mask correct at
      !! a periodic seam — the physical seam face is an interior index).
      !! And BEFORE `ocean_state_enter_data`: the host fill is what the
      !! `copyin` captures, and `metrics_closed_faces_alloc` reallocs.
      type(config_t), intent(in) :: cfg
      type(ocean_state_t), intent(inout) :: ocean_state
      type(hgrid_t), intent(in) :: grid
      integer, intent(in) :: compute_rank
      integer, intent(out), optional :: ierr
         !! Non-zero on a configuration conflict when present; absent
         !! behaves as today (`error stop`).

      integer :: nx, ny, nz, i, j, k
      integer :: n_closed_u, n_closed_v, n_open_u, n_open_v, n_ledge
      real(wp) :: h_face, sum_all, sum_open
      real(wp), allocatable :: tgt(:, :, :)
      character(len=16) :: vcoord_label

      if (present(ierr)) ierr = OCEAN_STATUS_OK
      if (.not. cfg%zfixed_closed_faces) then
         call refuse_open_zfixed_staircase(ocean_state, grid, ierr)
         return
      end if

      if (.not. ocean_state%multilayer%is_init) then
         call fail("&vcoord_nml zfixed_closed_faces requires the ocean "// &
                   "multilayer path (the mask is per-layer)", ierr, OCEAN_STATUS_ERR_SETUP)
         return
      end if
      if (ocean_state%vcoord%coord_type /= VCOORD_Z_FIXED .and. &
          ocean_state%vcoord%coord_type /= VCOORD_ZSTAR .and. &
          ocean_state%vcoord%coord_type /= VCOORD_ZSTAR_FULL) then
         call fail("&vcoord_nml zfixed_closed_faces is only defined for "// &
                   "vcoord_type='z_fixed', 'zstar' and 'zstar_full': the "// &
                   "live/filler staircase it closes is made by a GEOMETRIC "// &
                   "coordinate that vanishes bed-side layers at fixed "// &
                   "reference depths. On sigma / zstar_sigma every layer is live on "// &
                   "every wet face, and rho / hycom vanish layers by DENSITY, "// &
                   "so their pattern is not static", &
                   ierr, OCEAN_STATUS_ERR_SETUP)
         return
      end if
      if ((ocean_state%vcoord%coord_type == VCOORD_Z_FIXED .or. &
           ocean_state%vcoord%coord_type == VCOORD_ZSTAR) .and. &
          ocean_state%vcoord%z_fixed_h_ref <= 0.0_wp) then
         call fail("&vcoord_nml zfixed_closed_faces needs a resolved "// &
                   "z_fixed_h_ref (set &ocean_topo_nml max_depth): without "// &
                   "it the z_fixed / zstar target degenerates to uniform sigma, "// &
                   "there are no fillers, and the mask would close nothing", &
                   ierr, OCEAN_STATUS_ERR_SETUP)
         return
      end if
      if (ocean_state%vcoord%coord_type == VCOORD_ZSTAR_FULL .and. &
          ocean_state%vcoord%zstar_h_surf_target <= 0.0_wp) then
         call fail("&vcoord_nml zfixed_closed_faces under vcoord_type="// &
                   "'zstar_full' needs zstar_h_surf_target > 0: without it "// &
                   "build_zref_full lays uniform sigma, there are no fillers, "// &
                   "and the mask would close nothing", &
                   ierr, OCEAN_STATUS_ERR_SETUP)
         return
      end if
      if (ocean_state%dyn%bt_work%is_init) then
         if (maxval(ocean_state%dyn%bt_work%bt_H_ref) <= 0.0_wp) then
            call fail("&vcoord_nml zfixed_closed_faces: bt_H_ref is empty — "// &
                      "the barotropic datum must be built (&ocean_bt_nml "// &
                      "n_inner >= 1) before the static face mask can be laid", &
                      ierr, OCEAN_STATUS_ERR_SETUP)
            return
         end if
      end if
      if (cfg%ocean%wetdry%enable) then
         call fail("&vcoord_nml zfixed_closed_faces is incompatible with "// &
                   "&ocean_wetdry_nml enable: wet/dry moves the live/filler "// &
                   "pattern under the running state and the mask is static", &
                   ierr, OCEAN_STATUS_ERR_SETUP)
         return
      end if
      if (cfg%ocean%bt%bt_halo > 0) then
         call fail("&vcoord_nml zfixed_closed_faces is incompatible with "// &
                   "&ocean_bt_nml bt_halo > 0: the wide-halo barotropic "// &
                   "clone carries its own metrics and no face mask, so the "// &
                   "wide BT loop would transport through closed faces "// &
                   "(the same argument &ocean_porous_nml already makes)", &
                   ierr, OCEAN_STATUS_ERR_SETUP)
         return
      end if
      ! GM composes: `gm_column_x/y` build the streamfunction on each face's
      ! OPEN column (`metrics%open_u/open_v` x live on both sides) and the
      ! slopes slot masks slope / N^2 to it, so `uhD`/`vhD` are zero on
      ! every closed face-layer by construction (`test_ocean_gm_zfixed`).
      ! Redi composes: each face pairs only its OPEN WINDOW (the contiguous
      ! open + live-both-sides run), so the neutral-surface sweep, the PPM
      ! reconstruction and the flux scatter never touch a closed face-layer
      ! or a filler (`test_ocean_redi_zfixed`).
      ! Fox-Kemper MLE composes: the ML walk skips fillers and each face
      ! builds its overturning on its OPEN column (masked face thickness,
      ! `H_vel` clamped to it), so `uhml`/`vhml` — folded into
      ! mass_flux_*_layer after the per-layer mask — are zero on every
      ! closed face-layer and filler and still sum to zero per face
      ! (`test_ocean_mle_zfixed`).
      ! The velocity-form BIHARMONIC (scalar `nu_4` and the flow-aware
      ! `smag_ah` / `leith_biharm` `nu4_face_*`) composes: both chained
      ! Laplacians gate every difference by `open(a)*open(b)` (free-slip,
      ! `hvisc_biharm_lap_closed`).  The stress-tensor assembly (and
      ! `kh_aniso`, which only it reads) does not: its T-cell tension and
      ! corner shear are built from the face velocities with the 2-D
      ! `wet_*` masks only.
      if (cfg%ocean%hvisc%stress_tensor) then
         call fail("&vcoord_nml zfixed_closed_faces does not yet compose "// &
                   "with &ocean_hvisc_nml stress_tensor (nor kh_aniso, which "// &
                   "only the stress-tensor path reads): its T-cell tension and "// &
                   "corner shear are masked by the 2-D wet_* fields only, so a "// &
                   "closed face-layer's zeroed velocity enters the strain as a "// &
                   "Dirichlet value (no-slip at every staircase step).  The "// &
                   "velocity-Laplacian harmonic kernels and both biharmonic "// &
                   "paths (nu_4, smag_ah, leith_biharm) carry the free-slip "// &
                   "closure; use those", &
                   ierr, OCEAN_STATUS_ERR_SETUP)
         return
      end if
      ! ---- Barotropic paths still on FULL-column weights ----
      !
      ! `derive_bt_from_layers`, `face_depth_mean_*`, `set_cor_ref_velocity`,
      ! the `apply_bt_correction` fold, `compute_h_face_upstream`
      ! (`upstream_h_face`), `compute_bt_rem` (`substep_drag`) and
      ! `compute_bt_rem_wave_drag` (`wave_drag`) weight by `h_face·open`
      ! under this knob.  The bc-PGF correction does NOT: it sums
      ! `0.5·(h_L + h_R)` over EVERY layer.  Its answer is not wrong by a
      ! round-off — the closed layers' thickness is O(h_nominal) at a
      ! staircase face — so it is refused until it is ported, rather than
      ! left to run on the wrong column.
      if (cfg%ocean%bt%correction_bc_pgf) then
         call fail("&vcoord_nml zfixed_closed_faces does not yet compose "// &
                   "with &ocean_bt_nml correction_bc_pgf: compute_pbce, "// &
                   "compute_gtot_faces and the bc-PGF du_bc block in "// &
                   "apply_bt_correction all weight by the FULL column "// &
                   "(0.5*(h_L+h_R) over every layer), so the correction's "// &
                   "depth-mean-zero identity holds on the full column, not "// &
                   "on the OPEN column ubt is the mean of — it would leak a "// &
                   "barotropic increment into the open layers and push one "// &
                   "into the closed ones.  Set correction_bc_pgf=.false.", &
                   ierr, OCEAN_STATUS_ERR_SETUP)
         return
      end if

      nx = grid%nx_total
      ny = grid%ny_total
      nz = ocean_state%multilayer%nz_ml
      vcoord_label = "z_fixed"
      if (ocean_state%vcoord%coord_type == VCOORD_ZSTAR_FULL) vcoord_label = "zstar_full"
      if (ocean_state%vcoord%coord_type == VCOORD_ZSTAR) vcoord_label = "zstar"

      call metrics_closed_faces_alloc(ocean_state%metrics, grid, nz)

      ! The eta = 0 target of the running coordinate — `z_fixed`, `zstar`
      ! or `zstar_full` — through the SAME kernel the ALE regrid dispatches
      ! to, so "live" has one definition.  Under `zstar_full` it walks the
      ! per-column `z_ref` table the engine rebuilt from the
      ! halo-exchanged bathymetry just before the static-geometry pass.
      allocate (tgt(nx, ny, nz), source=0.0_wp)
      call ocean_vcoord_eta0_target(ocean_state%vcoord, tgt, &
                                    ocean_state%dyn%bt_work%bt_H_ref, nx, ny, nz)
      call ocean_vcoord_closed_face_masks(ocean_state%metrics%open_u, &
                                          ocean_state%metrics%open_v, &
                                          tgt, nx, ny, nz, H_VANISHED)
      ! The builder cannot evaluate the OUTERMOST face of the array (it
      ! needs a cell beyond it) and leaves it open.  On a tile that face
      ! is a SEAM ghost, three cells from the owned region, and on a
      ! periodic edge it is the wrap of an interior face — so without
      ! this exchange the ghost band of the mask is decomposition-
      ! dependent.  The harmonic kernels never read that deep; the
      ! biharmonic's chained stencil does (its ghost-band tendency at
      ! depth 1 reads the mask at depth 3), which broke 1x4 bit-identity
      ! in `test_ocean_decomp_bitid_mpi` (`file_readers`).  A plain face
      ! exchange (no sign flip: a 0/1 mask, not a vector) makes every
      ! ghost face carry its owner's value.  Single-rank non-periodic:
      ! a no-op.
      if (ocean_halo_is_init()) then
         call ocean_halo_face_x(ocean_state%metrics%open_u, nz, device_resident=.false.)
         call ocean_halo_face_y(ocean_state%metrics%open_v, nz, device_resident=.false.)
      end if

      ! Seed the BAROTROPIC face widths with the eta = 0 open-depth
      ! fraction.  `ocean_porous_refresh` recomputes them from the live
      ! `h` every outer step; this is only what stage 1 of step 1 reads.
      ! The width and the depth are the same number here: the BT
      ! transport is `ubt * FA * dy_cu_bt` with `FA = sum_k h_face`, so
      ! `dy_cu_bt = dy_cu * (sum_open h_face)/(sum_k h_face)` makes that
      ! product identically `ubt * (sum_open h_face) * dy_cu`.
      do j = 1, ny
         do i = 2, nx
            sum_all = 0.0_wp
            sum_open = 0.0_wp
            do k = 1, nz
               h_face = 0.5_wp*(tgt(i - 1, j, k) + tgt(i, j, k))
               sum_all = sum_all + h_face
               if (ocean_state%metrics%open_u(i, j, k) > 0.0_wp) then
                  sum_open = sum_open + h_face
               end if
            end do
            if (sum_all > 0.0_wp) then
               ocean_state%metrics%dy_cu_bt(i, j) = &
                  ocean_state%metrics%dy_cu(i, j)*(sum_open/sum_all)
            end if
         end do
      end do
      do j = 2, ny
         do i = 1, nx
            sum_all = 0.0_wp
            sum_open = 0.0_wp
            do k = 1, nz
               h_face = 0.5_wp*(tgt(i, j - 1, k) + tgt(i, j, k))
               sum_all = sum_all + h_face
               if (ocean_state%metrics%open_v(i, j, k) > 0.0_wp) then
                  sum_open = sum_open + h_face
               end if
            end do
            if (sum_all > 0.0_wp) then
               ocean_state%metrics%dx_cv_bt(i, j) = &
                  ocean_state%metrics%dx_cv(i, j)*(sum_open/sum_all)
            end if
         end do
      end do

      ! Cheap one-shot census for the configure line: how much of the
      ! array the mask actually closes, and whether it isolated any
      ! water.  Host-side, once, O(nx*ny*nz) — not a per-step diagnostic.
      n_closed_u = 0
      n_open_u = 0
      do k = 1, nz
         do j = 1, ny
            do i = 2, nx
               if (ocean_state%metrics%open_u(i, j, k) > 0.0_wp) then
                  n_open_u = n_open_u + 1
               else
                  n_closed_u = n_closed_u + 1
               end if
            end do
         end do
      end do
      n_closed_v = 0
      n_open_v = 0
      do k = 1, nz
         do j = 2, ny
            do i = 1, nx
               if (ocean_state%metrics%open_v(i, j, k) > 0.0_wp) then
                  n_open_v = n_open_v + 1
               else
                  n_closed_v = n_closed_v + 1
               end if
            end do
         end do
      end do
      n_ledge = ocean_vcoord_count_ledges(ocean_state%metrics%open_u, &
                                          ocean_state%metrics%open_v, &
                                          tgt, nx, ny, nz, H_VANISHED)

      deallocate (tgt)

      ! Flip the switches LAST — every consumer branches on them, and the
      ! mask has to be in place before any of them can read a closed face.
      ocean_state%metrics%use_closed_faces = .true.
      ocean_state%vcoord%zfixed_closed_faces = .true.
      ocean_state%vdiff%zlevel_faces = .true.

      if (compute_rank == 0) then
         call logger%info(trim(vcoord_label)//" closed faces: ON (partial steps, "// &
                          "Adcroft/Hill/Marshall 1997) — u closed "// &
                          to_string(n_closed_u)//"/"// &
                          to_string(n_closed_u + n_open_u)//", v closed "// &
                          to_string(n_closed_v)//"/"// &
                          to_string(n_closed_v + n_open_v)// &
                          ", isolated ledge cells "//to_string(n_ledge))
         if (n_ledge > 0) then
            call logger%warning(trim(vcoord_label)//" closed faces: "//to_string(n_ledge)// &
                                " LIVE cells have all four own-layer faces "// &
                                "closed — the mask has isolated water.  They "// &
                                "are inert (no flux in or out, velocity zeroed "// &
                                "every stage) but a one-cell spike in the bed "// &
                                "or the draft is worth looking at.")
         end if
      end if
      if (present(ierr)) ierr = OCEAN_STATUS_OK
   end subroutine configure_ocean_closed_faces

   subroutine refuse_open_zfixed_staircase(ocean_state, grid, ierr)
      !! The `zfixed_closed_faces = .false.` leg of
      !! `configure_ocean_closed_faces`: refuse `vcoord_type = "z_fixed"`
      !! with OPEN staircase faces over a STEPPED bed.
      !!
      !! A face where the two columns' bed falls in different nominal
      !! `z_fixed` layers pairs a live layer with a bed filler.  Left
      !! open, the FV pressure gradient across that step drives
      !! `|ρ′|·g·Δz_step/(ρ₀·dx)` out of rest whatever the filler
      !! thickness — measured on the 1° Southern Ocean: u ≈ 250 m/s by
      !! step 3, 4e9 m/s on the filler faces, `h → −2e9`, the first
      !! non-finite in `post_bt`, with or without any closure.  That is
      !! the case the closed-face mask exists for, so the combination
      !! fails loud here instead of blowing up three steps in.
      !!
      !! A flat or step-free bed is ACCEPTED untouched (no state is
      !! written, so it stays bit-identical), and so is every coordinate
      !! but `z_fixed`.  Top-side (ice-draft) steps are not counted.
      !!
      !! MPI: each rank counts its OWNED faces off the same GHOST-FILLED
      !! `bt_H_ref` / `z_top` the mask builder reads (so a tile-seam
      !! step is seen by exactly one rank) and the count is summed over
      !! the compute communicator, so every rank takes the same decision.
      !! Every early return is on rank-uniform state, so either all
      !! ranks reach the collective or none does.
      type(ocean_state_t), intent(in) :: ocean_state
      type(hgrid_t), intent(in) :: grid
      integer, intent(out), optional :: ierr

      integer :: nx, ny, nz, ng, n_local
      real(wp) :: h_nominal, steps_local, steps_global
      real(wp), allocatable :: tgt(:, :, :), eta0(:, :)

      if (present(ierr)) ierr = OCEAN_STATUS_OK
      if (.not. ocean_state%multilayer%is_init) return
      if (ocean_state%vcoord%coord_type /= VCOORD_Z_FIXED) return
      if (ocean_state%vcoord%z_fixed_h_ref <= 0.0_wp) return
      if (.not. ocean_state%dyn%bt_work%is_init) return

      nx = grid%nx_total
      ny = grid%ny_total
      ng = grid%nghost
      nz = ocean_state%multilayer%nz_ml
      h_nominal = ocean_state%vcoord%z_fixed_h_ref/real(nz, wp)

      allocate (tgt(nx, ny, nz), source=0.0_wp)
      allocate (eta0(nx, ny), source=0.0_wp)
      call ocean_vcoord_z_fixed_target(tgt, ocean_state%dyn%bt_work%bt_H_ref, &
                                       eta0, ocean_state%vcoord%z_top, &
                                       nx, ny, nz, h_nominal, &
                                       ocean_state%vcoord%z_fixed_use_profile, &
                                       ocean_state%vcoord%z_fixed_zi, &
                                       ocean_state%vcoord%z_fixed_dz, &
                                       ocean_state%vcoord%zstar_h_min)
      n_local = ocean_vcoord_count_bed_steps(tgt, ocean_state%dyn%bt_work%bt_H_ref, &
                                             ocean_state%metrics%dy_cu, &
                                             ocean_state%metrics%dx_cv, &
                                             nx, ny, nz, ng + 1, ng + grid%nx_phys, &
                                             ng + 1, ng + grid%ny_phys, H_VANISHED)
      deallocate (tgt, eta0)

      ! An integer count rides the real64 sum exactly (far below 2**53).
      steps_local = real(n_local, wp)
      call halo_allreduce_sum(steps_local, steps_global)
      if (steps_global < 0.5_wp) return

      call fail("&vcoord_nml zfixed_closed_faces = .false. is refused with "// &
                "vcoord_type='z_fixed' over a stepped bed: "// &
                to_string(nint(steps_global))//" wet faces join columns whose "// &
                "bed lies in different nominal z_fixed layers, so an open face "// &
                "pairs a live layer with a bed filler and the pressure gradient "// &
                "across the open staircase step drives flow from rest (blows "// &
                "up within steps).  Set &vcoord_nml zfixed_closed_faces = .true.", &
                ierr, OCEAN_STATUS_ERR_SETUP)
   end subroutine refuse_open_zfixed_staircase

   subroutine configure_ocean_porous(cfg, ocean_state, grid, compute_rank, ierr)
      !! Configure porous barriers (`&ocean_porous_nml`, Adcroft 2013).
      !! Grows the `ocean_metrics_t` porous arrays to full face size and
      !! fills the STATIC along-face `d_min`/`d_max`/`d_avg` statistics;
      !! the layer-averaged open fractions themselves are recomputed on
      !! the device every RK2 stage (they depend on the interface
      !! heights).
      !!
      !! MUST run AFTER bathymetry is set (`ocean_state%barotropic%b`)
      !! and land masking, and BEFORE `ocean_state_enter_data` — the
      !! realloc has to happen before the device map, and the host fill
      !! is what the `copyin` captures.  Leaves the placeholder-sized
      !! arrays untouched when the knob is off (bit-identical).
      type(config_t), intent(in) :: cfg
      type(ocean_state_t), intent(inout) :: ocean_state
      type(hgrid_t), intent(in) :: grid
      integer, intent(in) :: compute_rank
      integer, intent(out), optional :: ierr
         !! Non-zero on a porous-barrier configuration conflict when
         !! present; absent behaves as today (`error stop`).

      integer :: src, interp, nx, ny, nz

      if (.not. cfg%ocean%porous%enable) then
         if (present(ierr)) ierr = OCEAN_STATUS_OK
         return
      end if

      if (.not. ocean_state%multilayer%is_init) then
         call fail("&ocean_porous_nml enable requires the ocean multilayer "// &
                   "path (the open fractions are per-layer)", ierr, OCEAN_STATUS_ERR_SETUP)
         return
      end if

      src = parse_porous_source(cfg%ocean%porous%source)
      select case (src)
      case (POROUS_SOURCE_RESOLVED)
         continue
      case (POROUS_SOURCE_FILE)
         call fail("&ocean_porous_nml source='file' (an offline "// &
                   "subgrid-bathymetry file, MOM6 topog_edge.nc) is not "// &
                   "implemented — it needs the file-forcing backend that "// &
                   "&ocean_bt_nml wave_drag_form='file' also waits on. "// &
                   "Use source='resolved' (a documented proxy).", ierr, OCEAN_STATUS_ERR_SETUP)
         return
      case default
         call fail("&ocean_porous_nml source='"// &
                   trim(cfg%ocean%porous%source)//"' is not recognised", ierr, OCEAN_STATUS_ERR_SETUP)
         return
      end select

      interp = parse_porous_eta_interp(cfg%ocean%porous%eta_interp)
      if (interp < 0) then
         call fail("&ocean_porous_nml eta_interp='"// &
                   trim(cfg%ocean%porous%eta_interp)//"' is not recognised", ierr, OCEAN_STATUS_ERR_SETUP)
         return
      end if

      nx = grid%nx_total
      ny = grid%ny_total
      nz = ocean_state%multilayer%nz_ml

      call metrics_porous_alloc(ocean_state%metrics, grid, nz)
      ocean_state%metrics%porous_eta_interp = interp
      ! The namelist knob is a DEPTH (positive below the surface); the
      ! kernel gate compares HEIGHTS, so flip the sign once here.
      ocean_state%metrics%porous_mask_depth = -cfg%ocean%porous%masking_depth

      ! SIGN CONVENTION.  The Adcroft fit works in topographic HEIGHTS
      ! (positive up, negative below the sea surface) so that they compare
      ! directly against interface heights.  On the ocean path
      ! `barotropic%b` holds the reference column DEPTH, positive down
      ! (`SSH = sum(h_layer) - b`), so it is negated once, here, and every
      ! porous array downstream is a height.  Bathymetry is static, so a
      ! snapshot on the metrics slot is exact and lets the per-step
      ! recompute stay a metrics-only kernel.
      ocean_state%metrics%por_bed = -ocean_state%barotropic%b

      call porous_fill_stats_resolved(nx, ny, ocean_state%metrics%por_bed, &
                                      ocean_state%metrics%wet_T, &
                                      ocean_state%metrics%por_dmin_u, &
                                      ocean_state%metrics%por_dmax_u, &
                                      ocean_state%metrics%por_davg_u, &
                                      ocean_state%metrics%por_dmin_v, &
                                      ocean_state%metrics%por_dmax_v, &
                                      ocean_state%metrics%por_davg_v)

      ! READER-BOUNDARY ASSERTION.  `m = (d_avg-d_min)/(d_max-d_min)` is
      ! only a valid Adcroft parameter when `d_min <= d_avg <= d_max`.
      ! The resolved fill cannot break that (d_avg is a convex combination
      ! of the samples d_min/d_max bracket), but a FILE-backed source
      ! could, so the check sits here — at the boundary every source
      ! passes through — rather than inside one filler.
      if (.not. porous_stats_are_ordered(nx + 1, ny, &
                                         ocean_state%metrics%por_dmin_u, &
                                         ocean_state%metrics%por_dmax_u, &
                                         ocean_state%metrics%por_davg_u)) then
         call fail("&ocean_porous_nml: u-face subgrid statistics violate "// &
                   "d_min <= d_avg <= d_max", ierr, OCEAN_STATUS_ERR_SETUP)
         return
      end if
      if (.not. porous_stats_are_ordered(nx, ny + 1, &
                                         ocean_state%metrics%por_dmin_v, &
                                         ocean_state%metrics%por_dmax_v, &
                                         ocean_state%metrics%por_davg_v)) then
         call fail("&ocean_porous_nml: v-face subgrid statistics violate "// &
                   "d_min <= d_avg <= d_max", ierr, OCEAN_STATUS_ERR_SETUP)
         return
      end if

      ! Flip the master switch LAST: every kernel branches on it, and the
      ! stats must be in place before any of them can read a narrowed width.
      ocean_state%metrics%use_porous = .true.

      if (compute_rank == 0) then
         call logger%info("Porous barriers:  ON (Adcroft 2013), source='"// &
                          trim(cfg%ocean%porous%source)//"' eta_interp='"// &
                          trim(cfg%ocean%porous%eta_interp)//"' masking_depth="// &
                          to_string(cfg%ocean%porous%masking_depth)//" m")
         call logger%info("                  source='resolved' is an along-face "// &
                          "statistic of the RESOLVED bathymetry, a documented "// &
                          "proxy for true subgrid data")
      end if
      if (present(ierr)) ierr = OCEAN_STATUS_OK
   end subroutine configure_ocean_porous

   subroutine wave_drag_roughness_proxy(b, wet_T, nx, ny, nghost, kappa, n_bot, h2_max, r_h)
      !! `form="roughness_proxy"` filler — a DOCUMENTED PLACEHOLDER for
      !! Jayne & St Laurent (2001)'s subgrid `<h^2>`, not a substitute for
      !! it (that needs PR-14's file reader or PR-30's field-valued
      !! roughness). Estimates the subgrid topographic-height variance from
      !! the RESOLVED 2-delta bathymetry increment:
      !!     <h^2>_proxy(i,j) = 1/4*[(b(i+1,j)-b(i-1,j))^2 + (b(i,j+1)-b(i,j-1))^2]
      !! then `r_H = 1/2*kappa*min(<h^2>_proxy, h2_max)*N_bot`. `b` is
      !! bottom elevation, positive UP (`rdb_barotropic_state.F90`) —
      !! differences are sign-independent. Zero on land (`wet_T==0`) and on
      !! the ghost ring (the 2-delta stencil is unavailable there; a
      !! formula-bathymetry path that leaves ghosts unfilled would
      !! otherwise manufacture a spurious cliff at the ghost seam — CLAUDE.md
      !! "Formula bathymetry setters must fill ghost rows").
      integer, intent(in) :: nx, ny, nghost
      real(wp), intent(in) :: b(nx, ny), wet_T(nx, ny)
      real(wp), intent(in) :: kappa, n_bot, h2_max
      real(wp), intent(inout) :: r_h(nx, ny)
      integer :: i, j
      real(wp) :: h2

      r_h = 0.0_wp
      do j = nghost + 1, ny - nghost
         do i = nghost + 1, nx - nghost
            if (wet_T(i, j) <= 0.0_wp) cycle
            h2 = 0.25_wp*((b(i + 1, j) - b(i - 1, j))**2 + (b(i, j + 1) - b(i, j - 1))**2)
            r_h(i, j) = 0.5_wp*kappa*min(h2, h2_max)*n_bot
         end do
      end do
   end subroutine wave_drag_roughness_proxy

   subroutine parse_constituent_list(list, cat_idx, nconst, ierr)
      !! Tokenize a whitespace/comma-separated constituent list into
      !! catalog indices (case-insensitive).  Unknown tokens fail loud;
      !! `nconst` is the count of recognised entries.
      character(len=*), intent(in) :: list
      integer, intent(out) :: cat_idx(:)
      integer, intent(out) :: nconst
      integer, intent(out), optional :: ierr
         !! Non-zero on an unrecognised constituent token when present;
         !! absent behaves as today (`error stop`).
      integer :: i, n, i0, idx
      character(len=len(list)) :: buf
      logical :: in_tok

      buf = list
      ! Normalise commas to spaces.
      do i = 1, len(buf)
         if (buf(i:i) == ",") buf(i:i) = " "
      end do

      nconst = 0
      n = len_trim(buf)
      in_tok = .false.
      i0 = 1
      do i = 1, n + 1
         if (i <= n .and. buf(i:i) /= " ") then
            if (.not. in_tok) then
               in_tok = .true.
               i0 = i
            end if
         else
            if (in_tok) then
               in_tok = .false.
               idx = tide_name_index(buf(i0:i - 1))
               if (idx < 1) then
                  call fail("ocean_tides_nml: unknown constituent '"// &
                            trim(buf(i0:i - 1))//"'", ierr, OCEAN_STATUS_ERR_SETUP)
                  return
               end if
               nconst = nconst + 1
               cat_idx(nconst) = idx
            end if
         end if
      end do
      if (present(ierr)) ierr = OCEAN_STATUS_OK
   end subroutine parse_constituent_list

   subroutine configure_ocean_wavespeed(cfg, ocean_state, grid, ierr)
      !! Copy the `&ocean_wavespeed_nml` knobs onto the wave-speed slot
      !! (B1).  Diagnostic, no mutual exclusion: cg1/Rd read `rho_layer`
      !! directly.  Fills `f_centre` + the static `beta_centre = |grad
      !! f|` field via `fill_coriolis_centre` + `build_static` — the
      !! SAME `metrics_fill_coriolis` path VarMix/MEKE use (planetary on
      !! spherical/tripolar, beta-plane bit-identical elsewhere) —
      !! rather than the legacy hard-coded beta-plane `set_f_centre`.
      !! Copies rho0 from the EOS slot for the Boussinesq `gprime`.  Must
      !! run AFTER `configure_ocean_metrics` (metrics filled + finalized).
      type(config_t), intent(in) :: cfg
      type(ocean_state_t), intent(inout) :: ocean_state
      type(hgrid_t), intent(in) :: grid
      integer, intent(out), optional :: ierr
         !! Non-zero on a wave-speed configuration conflict when present;
         !! absent behaves as today (`error stop`).
      real(wp), allocatable :: f_centre(:, :)

      associate (ws => ocean_state%wavespeed, wcfg => cfg%ocean%wavespeed)
         ws%enable = wcfg%enable
         if (.not. wcfg%enable) then
            if (present(ierr)) ierr = OCEAN_STATUS_OK
            return
         end if

         ! Fail loud at configure: the column kernel indexes fixed-size
         ! NZ_STACK_MAX column arrays; wavespeed_compute never ran before
         ! this PR so nz > NZ_STACK_MAX never overran it.  It can now.
         if (cfg%nz_layers > NZ_STACK_MAX) then
            call fail("ocean_wavespeed_nml: nz_layers exceeds "// &
                      "NZ_STACK_MAX (raise NZ_STACK_MAX in rdb_constants)", ierr, OCEAN_STATUS_ERR_SETUP)
            return
         end if

         ws%mono_n2_depth = wcfg%mono_n2
         ws%use_ebt = wcfg%use_ebt
         ws%n_wavespeed = wcfg%n_wavespeed
         ws%rho0 = ocean_state%eos%rho0

         ! |f| at centres + the static |grad f| field for Rd.
         allocate (f_centre(grid%nx_total, grid%ny_total))
         call fill_coriolis_centre(cfg, ocean_state%metrics, grid, f_centre)
         call ws%build_static(grid, ocean_state%metrics, f_centre)
         deallocate (f_centre)

         if (wcfg%n_wavespeed < 1) then
            call fail("ocean_wavespeed_nml: n_wavespeed must be >= 1", ierr, OCEAN_STATUS_ERR_SETUP)
            return
         end if
      end associate
      if (present(ierr)) ierr = OCEAN_STATUS_OK
   end subroutine configure_ocean_wavespeed

   subroutine configure_ocean_varmix(cfg, ocean_state, grid)
      !! Build the STATIC VarMix grid terms (`f2_dx2_*`, `beta_dx2_*`,
      !! `l2_*`) on the varmix slot once at configure time, from the filled
      !! curvilinear metrics + the cell-centre Coriolis magnitude.  The slot
      !! scalars are copied earlier by `ocean_state_copy_config`; this only
      !! fills the static arrays (the per-step Res_fn/SN/assembly fire in the
      !! dyn step).  No-op when VarMix is disabled.
      type(config_t), intent(in) :: cfg
      type(ocean_state_t), intent(inout) :: ocean_state
      type(hgrid_t), intent(in) :: grid
      real(wp), allocatable :: f_centre(:, :)

      if (.not. ocean_state%varmix%enable) return
      allocate (f_centre(grid%nx_total, grid%ny_total))
      call fill_coriolis_centre(cfg, ocean_state%metrics, grid, f_centre)
      call ocean_state%varmix%build_static(ocean_state%metrics, f_centre)
      deallocate (f_centre)
   end subroutine configure_ocean_varmix

   subroutine configure_ocean_meke(cfg, ocean_state, grid)
      !! Fill the MEKE slot's cell-centre |Coriolis| from the same
      !! `metrics_fill_coriolis` path VarMix / EPBL use (handles beta-plane
      !! AND spherical), so `beta = |grad f|` for the Rhines length is live
      !! when `alpha_rhines > 0`.  The MEKE scalar knobs are copied earlier
      !! by `ocean_state_copy_config`; this only fills `f_centre`.  Host loop
      !! before `enter_data`.  No-op when MEKE is disabled.
      type(config_t), intent(in) :: cfg
      type(ocean_state_t), intent(inout) :: ocean_state
      type(hgrid_t), intent(in) :: grid
      real(wp), allocatable :: f_centre(:, :)

      if (.not. ocean_state%meke%enable) return
      allocate (f_centre(grid%nx_total, grid%ny_total))
      call fill_coriolis_centre(cfg, ocean_state%metrics, grid, f_centre)
      call ocean_state%meke%set_f_centre(grid, f_centre)
      deallocate (f_centre)
   end subroutine configure_ocean_meke

   subroutine configure_ocean_reference_density(ocean_state, geo)
      !! Fan the ONE configured Boussinesq reference density out to every
      !! remaining slot that carries its own `rho0` copy.
      !!
      !! `&ocean_ic_nml rho_0` lands on `eos%rho0` in
      !! `ocean_state_init_from_config`; that is the ρ₀ of record.  EPBL,
      !! kappa-shear, tidal mixing, wave speed, the `eta_ib` surface-pressure
      !! seam, GM / MEKE / Redi / MLE, the isopycnal slopes and (since the
      !! preceding commit) the PGF all copy it in their own
      !! `configure_ocean_*`.  The four slots wired here were the remainder:
      !! they kept a hard 1035 type default that nothing ever assigned, so a
      !! namelist with `rho_0 /= 1035` ran the EOS on one reference density
      !! and the surface forcing, wind stress and KPP buoyancy on another —
      !! no warning, no fail-loud, just a 1035/ρ₀ scaling on every surface
      !! heat/salt flux, every wind-stress acceleration, and N²/u*/B_0.
      !!
      !!   * `surface_flux%rho0`  — the `dt/(ρ₀·cp)` heat and `dt/ρ₀` salt
      !!     divisors for EVERY surface tracer source, including what the
      !!     sea-ice coupler delivers through `Q_heat`/`Q_salt`.
      !!   * `surface_stress%rho0` — the `τ/(ρ₀·h_top)` acceleration (both
      !!     the top-layer and the DIRECT_STRESS distributed form).
      !!   * `vmix%rho0` — KPP: N² = −g/ρ₀·∂ρ/∂z, u* = √(|τ|/ρ₀), the
      !!     kinematic surface fluxes q_T = Q_heat/(ρ₀·cp), q_S = Q_salt/ρ₀
      !!     feeding B_0, and the PP81 / convective-adjustment N².
      !!   * `geothermal%rho0` — the `dt·Q_geo/(ρ₀·cp)` bed heat source.
      !!     The geothermal slot lives on the engine, not on `ocean_state`,
      !!     so it is passed in (optional: a caller with no geothermal slot
      !!     simply omits it).
      !!
      !! Device contract (`mem:separate`): `surface_flux`, `surface_stress`
      !! and `geothermal` read their `rho0` HOST-side (folded into the
      !! `inv_scale` / `src_T` scalar or passed by value into a `*_impl`), so
      !! those owe nothing.  `vmix%rho0` IS read on-device — `this%rho0`
      !! appears inside the `do concurrent` bodies of `vmix_compute_pp81`,
      !! `vmix_kpp_overlay_impl` and `vmix_convective_impl` — but this
      !! routine runs in the configure phase, strictly BEFORE
      !! `ocean_state_enter_data`'s `copyin`, exactly like the neighbouring
      !! `pp81_*` / `shear2_floor` scalars it sits with.  No
      !! `!$acc update device` is owed.  **A configure step that ever moves
      !! after `enter_data` must add one.**
      type(ocean_state_t), intent(inout) :: ocean_state
      type(ocean_geothermal_t), intent(inout), optional :: geo
         !! Engine-held geothermal slot (the split driver's `geo` argument).

      ocean_state%surface_flux%rho0 = ocean_state%eos%rho0
      ocean_state%surface_stress%rho0 = ocean_state%eos%rho0
      ocean_state%vmix%rho0 = ocean_state%eos%rho0
      if (present(geo)) geo%rho0 = ocean_state%eos%rho0
   end subroutine configure_ocean_reference_density

   subroutine configure_ocean_pgf(cfg, ocean_state, compute_rank, ierr)
      !! Pressure-force variant, the reference densities (`rho0` / `rho_ref`,
      !! both from the single configured ρ₀ — `&ocean_ic_nml rho_0` via
      !! `eos%rho0`), the reduced-gravity (gprime / gfs_scale) knobs, the
      !! bathymetry copy into the PGF slot, and the matching barotropic
      !! fast-loop gravity g_bt for the gprime / FV_MOM6-reduced-GFS paths.
      type(config_t), intent(in) :: cfg
      type(ocean_state_t), intent(inout) :: ocean_state
      integer, intent(in) :: compute_rank
         !! Unused (no rank-0 logging in this helper); kept for a uniform
         !! configure_ocean_* signature.
      integer, intent(out), optional :: ierr
         !! Non-zero on a PGF/EOS configuration conflict when present;
         !! absent behaves as today (`error stop`).
      integer :: local_ierr

      ! ALLOCATION-GATE CONSISTENCY GUARD.  When `scratch_gated` is on, the
      ! PGF slot allocated only the buffers reachable from the `variant` /
      ! `reconstruct_for_pressure` latched by `ocean_state_init_from_config`
      ! BEFORE `init(grid)`.  Both are re-derived here from the same cfg
      ! fields, so they must agree.  If a future edit ever lets them
      ! diverge, the gated-off buffers would be unallocated on a path that
      ! reaches them — which under `-gpu=mem:separate` is a silent wrong
      ! answer, not a crash.  Fail loud instead.
      if (ocean_state%pressure_force%scratch_gated) then
         if (ocean_state%pressure_force%variant /= &
             parse_opgf_variant(cfg%ocean%pgf%form)) then
            call fail("configure_ocean_pgf: pgf variant changed between "// &
                      "the pre-init latch and configure — the scratch "// &
                      "allocation gate was decided on the stale value.", ierr, OCEAN_STATUS_ERR_SETUP)
            return
         end if
         if (ocean_state%pressure_force%reconstruct_for_pressure .neqv. &
             cfg%ocean%pgf%reconstruct_for_pressure) then
            call fail("configure_ocean_pgf: reconstruct_for_pressure "// &
                      "changed between the pre-init latch and configure — "// &
                      "the recon scratch allocation gate is stale.", ierr, OCEAN_STATUS_ERR_SETUP)
            return
         end if
      end if
      ocean_state%pressure_force%variant = parse_opgf_variant(cfg%ocean%pgf%form)

      ! Grounded-layer PGF gate (`&ocean_isopycnal_nml pgf_skip_nonoverlap`).
      ! The predicate lives in `pgf_nonoverlap_gate_on` because the PGF slot's
      ! allocation gate had to evaluate it BEFORE `init` (FV_MOM6 allocates
      ! `z_centre` for this gate and nothing else) — see the pre-init latch in
      ! `ocean_state_init_from_config`.  Re-evaluating it here and
      ! comparing is what keeps that latch honest.
      if (ocean_state%pressure_force%scratch_gated .and. &
          (ocean_state%pressure_force%skip_nonoverlap .neqv. &
           pgf_nonoverlap_gate_on(cfg, ocean_state%pressure_force%variant))) then
         call fail("configure_ocean_pgf: pgf_skip_nonoverlap changed between "// &
                   "the pre-init latch and configure — the z_centre allocation "// &
                   "gate is stale.", ierr, OCEAN_STATUS_ERR_SETUP)
         return
      end if
      ocean_state%pressure_force%skip_nonoverlap = &
         pgf_nonoverlap_gate_on(cfg, ocean_state%pressure_force%variant)
      ! Grounded = non-overlapping AND vanished on one side: the gate never
      ! zeroes the PGF of a layer that is massive in both columns.
      ocean_state%pressure_force%nonoverlap_vanish_tol = &
         nonoverlap_vanish_tol_for(cfg%ocean%isopycnal%angstrom_h)
      if (ocean_state%pressure_force%skip_nonoverlap) then
         if (compute_rank == 0) then
            call logger%info("Isopycnal PGF:    pgf_skip_nonoverlap = ON "// &
                             "(grounded-layer face PGF zeroed where layers do not overlap "// &
                             "and one side is <= "// &
                             to_string(ocean_state%pressure_force%nonoverlap_vanish_tol)//" m)")
         end if
      else if (parse_ocean_vcoord_type(cfg%vcoord_type) == VCOORD_LAGRANGIAN .and. &
               cfg%ocean%isopycnal%pgf_skip_nonoverlap .and. &
               ocean_state%pressure_force%variant /= OPGF_VARIANT_GPRIME) then
         ! Asked for, but not available.  What is left here is `gprime`: the
         ! NK=2 reduced-gravity form differences interface positions directly,
         ! never builds the layer-centre Jacobian the gate corrects and carries
         ! no `z_centre` buffer — enabling the gate would read unallocated
         ! memory, a silent wrong answer under `-gpu=mem:separate` rather than
         ! a crash.  Loud, not fatal: the run is legal, it just keeps the
         ! spurious gradient.
         if (compute_rank == 0) then
            call logger%warning("ocean_isopycnal_nml: pgf_skip_nonoverlap has no "// &
                                "effect with ocean_pgf_nml form='"// &
                                trim(cfg%ocean%pgf%form)//"' (the gate covers mont "// &
                                "/ fv_lite / fv_wright / fv_mom6) — a grounded "// &
                                "isopycnal layer can still drive a spurious "// &
                                "pressure gradient at rest")
         end if
      end if

      ! N2: device-callable-variant gate runs POST-config (eos%variant was
      ! set from the knob in configure_ocean_drag, the first configure call).
      if (present(ierr)) then
         call eos_validate(ocean_state%eos, ierr=local_ierr)
         if (local_ierr /= 0) then
            ierr = local_ierr
            return
         end if
      else
         call eos_validate(ocean_state%eos)
      end if
      ! FV_WRIGHT re-evaluates the Wright (1997) rational EOS inside its
      ! Picard pressure sweep, so it cannot honour a non-Wright rho_layer.
      ! Roquet + FV_WRIGHT is therefore unsupported (FV_LITE / FV_MOM6 /
      ! gprime read rho_layer generically and are fine).  Fail loud — a
      ! follow-up could add a Roquet column sweep.
      if (ocean_state%eos%variant == EOS_VARIANT_ROQUET_SPV .and. &
          ocean_state%pressure_force%variant == OPGF_VARIANT_FV_WRIGHT) then
         call fail("configure_ocean_pgf: eos='roquet_spv' is "// &
                   "incompatible with pgf form='fv_wright' (the FV_WRIGHT "// &
                   "Picard sweep re-evaluates Wright internally). Use "// &
                   "fv_lite, fv_mom6, or gprime with roquet_spv.", ierr, OCEAN_STATUS_ERR_SETUP)
         return
      end if

      ! Reference densities.  BOTH PGF reference densities follow the SINGLE
      ! configured ρ₀ (`&ocean_ic_nml rho_0`, landed on `eos%rho0` by
      ! `ocean_state_init_from_config` — the same scalar EPBL, kappa-shear,
      ! tidal mixing, the `eta_ib` surface-pressure seam, MEKE/GM and the
      ! isopycnal slopes all take).  Without this the PGF slot kept its
      ! 1035 type default while the EOS followed the namelist, so a run with
      ! `rho_0 /= 1035` silently integrated an EOS and a pressure gradient on
      ! two different reference densities.
      !
      ! `rho0` (Boussinesq divisor, `du/dt = −(1/ρ₀)∂p/∂x`) and `rho_ref`
      ! (the baseline subtracted from layer densities when building the
      ! FV_MOM6 `pa` anomaly stack, and the `g·ρ_ref/ρ₀` surface value in
      ! `compute_pbce`) stay SEPARATE MEMBERS — the roles differ and
      ! interchanging them is a known MOM6 bug class — but roundabout has no
      ! separate anomaly-reference knob, so both take the one configured ρ₀.
      !
      ! Plain host scalars: every consumer reads them host-side (into a local
      ! `inv_rho0`, or by value into a `*_impl`), so no `!$acc update device`
      ! is owed here — and this runs well before `ocean_state_enter_data` in
      ! any case.
      ocean_state%pressure_force%rho0 = ocean_state%eos%rho0
      ocean_state%pressure_force%rho_ref = ocean_state%eos%rho0

      ocean_state%pressure_force%gprime_gfs = cfg%ocean%pgf%gprime_gfs
      ocean_state%pressure_force%gprime_gint = cfg%ocean%pgf%gprime_gint
      ocean_state%pressure_force%gfs_scale = cfg%ocean%pgf%gfs_scale
      ocean_state%pressure_force%mass_weight = cfg%ocean%pgf%mass_weight
      ocean_state%pressure_force%reconstruct_for_pressure = &
         cfg%ocean%pgf%reconstruct_for_pressure
      ocean_state%pressure_force%insitu_density = cfg%ocean%pgf%insitu_density
      ocean_state%pressure_force%recon_scheme = cfg%ocean%pgf%recon_scheme
      ocean_state%pressure_force%p_top_in_bc = cfg%ocean%pgf%p_top_in_bc
      ! The top-of-column load enters the `pa` stack's surface boundary
      ! condition, which only the FV_MOM6 family builds.  Mirrors the
      ! `validate_config` refusal so an in-memory namelist / API caller
      ! that bypasses validation still fails loud instead of running a
      ! silently inert knob.
      if (cfg%ocean%pgf%p_top_in_bc .and. &
          ocean_state%pressure_force%variant /= OPGF_VARIANT_FV_MOM6) then
         call fail("configure_ocean_pgf: p_top_in_bc=.true. requires "// &
                   "form='fv_mom6'. Other PGF variants build no pa(nz+1) "// &
                   "pressure-stack boundary condition for the load to enter.", &
                   ierr, OCEAN_STATUS_ERR_SETUP)
         return
      end if
      ! The bc-PGF retro-correction reads `pgf%e_face`, which only FV_MOM6
      ! fills.  Mirrors the `validate_config` refusal so an in-memory /
      ! API caller fails at configure, not with an `error stop` in step 1.
      if (cfg%ocean%bt%correction_bc_pgf .and. &
          ocean_state%pressure_force%variant /= OPGF_VARIANT_FV_MOM6) then
         call fail("configure_ocean_pgf: &ocean_bt_nml correction_bc_pgf=.true. "// &
                   "requires form='fv_mom6'. compute_pbce builds the per-layer "// &
                   "pressure response from the FV_MOM6 interface-height stack "// &
                   "(pgf%e_face), which no other PGF form fills.", &
                   ierr, OCEAN_STATUS_ERR_SETUP)
         return
      end if
      ! In-layer T/S reconstruction wires into the FV_MOM6 layer-integrated
      ! form only (it carries e_face / pa / intz_dpa).  Fail loud if the
      ! knob is on with any other PGF form.
      if (cfg%ocean%pgf%reconstruct_for_pressure .and. &
          ocean_state%pressure_force%variant /= OPGF_VARIANT_FV_MOM6) then
         call fail("configure_ocean_pgf: reconstruct_for_pressure=.true. "// &
                   "requires form='fv_mom6' (the layer-integrated FV path). "// &
                   "Other PGF variants do not carry the e_face/pa/intz_dpa "// &
                   "stack the reconstruction replaces.", ierr, OCEAN_STATUS_ERR_SETUP)
         return
      end if
      ! The PGF's bathymetry copy (gprime recovers ∇η = ∇(Σh) - ∇b, FV-MOM6
      ! places the bottom interface from it) is NOT taken here: `engine_setup`
      ! takes it after the init-time periodic/fold wrap + halo exchange of
      ! `b`, so the seam ghosts it copies are the right ones.
      ! gprime: run the BT fast loop at the reduced free-surface gravity g_FS,
      ! else the BT correction cancels the gprime reduced-gravity tendency.
      if (ocean_state%pressure_force%variant == OPGF_VARIANT_GPRIME) then
         ocean_state%dyn%bt_work%g_bt = ocean_state%pressure_force%gprime_gfs
      end if
      ! BEBT projection (MOM6 BT_PROJECT_VELOCITY); 0 = pure forward-backward.
      ocean_state%dyn%bt_work%bebt = cfg%ocean%bt%bebt
      ! FV_MOM6: BT gravity is ALWAYS gfs_scale*GRAVITY (the Pass-5
      ! Montgomery correction supplies the complementary share when
      ! gfs_scale < 1). Gate must NOT condition on nz_layers (NK=1 SSH
      ! bug, 2026-05-26).
      !
      ! UNCONDITIONAL on gfs_scale (fixed 2026-09-28, was gated on
      ! `gfs_scale < 1.0 - 1e-12`): `pgf_free_surface_gravity` builds the
      ! slow PGF's shed free-surface term as
      ! `gfs_scale*GRAVITY*rho_ref/rho0` unconditionally (GRAVITY =
      ! 9.80665, `rdb_constants`), but with the old gate `g_bt` stayed at
      ! `barotropic_workstate_t`'s struct-default literal `9.81_wp`
      ! whenever `gfs_scale >= 1` (the common default) -- a ~3.4e-4
      ! relative mismatch between the two gravities. `set_fast_forcing_eta_pf`
      ! subtracts `g_pf*grad(eta)` from the depth-mean slow PGF so the
      ! fast loop's own live `-g_bt*grad(eta)` can re-supply exactly that
      ! term; with `g_pf /= g_bt` the two do not cancel and the fast loop
      ! carries a real leftover forcing `(g_bt - g_pf)*grad(eta)`.
      ! Invisible in an ordinary run (eta gradients are generated by real
      ! dynamics, and the bias is a tiny wave-speed error), but exposed
      ! bit-for-bit whenever `eta` starts with a real static gradient at
      ! rest -- exactly what `&ocean_cavity_dyn_nml trim_ic_for_p_surf`
      ! seeds under a sloping ice draft. Root cause of the
      ! `vcm_lid_slope_sigma_unstrat` rest-state blip (peak En 9.997e-13);
      ! see `tests/test_ocean_cavity_load.F90::test_trim_uniform_rho_rest`
      ! (the end-to-end regression; the raw PGF face force was ALREADY
      ! round-off before this fix -- see the sibling
      ! `test_trim_balances_pfu_uniform_rho`).
      if (ocean_state%pressure_force%variant == OPGF_VARIANT_FV_MOM6) then
         ocean_state%dyn%bt_work%g_bt = ocean_state%pressure_force%gfs_scale*GRAVITY
      end if
      if (present(ierr)) ierr = OCEAN_STATUS_OK
   end subroutine configure_ocean_pgf

   subroutine configure_ocean_bt(cfg, ocean_state, grid, compute_rank)
      !! Barotropic-substep correction knobs (MOM6 frhatu h-weighting, bc-PGF
      !! retro-correction, bt_rem_u drag damping, visc_rem joint weight), the
      !! BT_cont_type / upstream-PPM h_face workspace allocations, and the
      !! rank-0 PGF/BT configuration log lines.
      type(config_t), intent(in) :: cfg
      type(ocean_state_t), intent(inout) :: ocean_state
      type(hgrid_t), intent(in) :: grid
      integer, intent(in) :: compute_rank

      ! MOM6 BT_force / eta_PF split: the depth-mean baroclinic PGF forces
      ! the barotropic substep (default on).
      ocean_state%dyn%bt_work%bt_bc_pgf_forcing = cfg%ocean%bt%bc_pgf_forcing
      if (compute_rank == 0 .and. .not. cfg%ocean%bt%bc_pgf_forcing) then
         call logger%warning("&ocean_bt_nml bc_pgf_forcing = .false.: LEGACY split — the "// &
                             "barotropic mode does not feel the depth-mean baroclinic "// &
                             "pressure gradient (no JEBAR / bottom-pressure torque)")
      end if

      ! MOM6 btstep_layer_accel — per-layer bc-PGF retro-correction (needs FV_MOM6).
      ocean_state%dyn%bt_work%bt_correction_bc_pgf = cfg%ocean%bt%correction_bc_pgf
      if (compute_rank == 0 .and. cfg%ocean%bt%correction_bc_pgf) then
         call logger%info("BT correction:    bc-PGF retro-correction ON "// &
                          "(MOM6 btstep_layer_accel; requires fv_mom6 PGF)")
      end if

      ! MOM6 bt_rem_u — multiplicative drag damping inside the BT substep.
      ocean_state%dyn%bt_work%bt_substep_drag = cfg%ocean%bt%substep_drag
      if (compute_rank == 0 .and. cfg%ocean%bt%substep_drag) then
         call logger%info("BT substep:       drag damping ON "// &
                          "(MOM6 bt_rem_u: r·HBBL/(Htot+r·HBBL·dt_bt))")
      end if

      ! PR-2 (bt-rem-from-av-rem): bt_rem_u/v from the SAME viscous
      ! remnant the layered momentum solve uses (MOM6's barotropic
      ! solver). Mutually exclusive with substep_drag and bt_halo > 0
      ! (validated at configure).
      ocean_state%dyn%bt_work%bt_rem_from_visc_rem = ocean_bt_rem_from_visc_rem_on(cfg)
      ocean_state%dyn%bt_work%bt_strong_drag = cfg%ocean%bt%strong_drag
      ocean_state%dyn%bt_work%bt_rescale_strong_drag = cfg%ocean%bt%rescale_strong_drag

      ! frhat port (&ocean_bt_nml frhat_scheme): the per-layer face-thickness
      ! closure every barotropic depth mean reads (MOM6 btcalc HYBRID vs the
      ! plain two-abutting-cell arithmetic mean). Default "arithmetic" ==
      ! bit-identical to the pre-port tree.
      ocean_state%dyn%bt_work%frhat_scheme = parse_frhat_scheme(cfg%ocean%bt%frhat_scheme, &
                                                                FRHAT_ARITHMETIC)
      if (compute_rank == 0 .and. ocean_state%dyn%bt_work%frhat_scheme == FRHAT_HYBRID) then
         call logger%info("BT frhat:         hybrid ON "// &
                          "(MOM6 btcalc HVEL_SCHEME=HYBRID face-thickness closure)")
      end if
      if (compute_rank == 0 .and. ocean_bt_rem_from_visc_rem_on(cfg)) then
         block
            character(len=:), allocatable :: bt_rem_msg
            bt_rem_msg = "BT substep:       bt_rem = av_rem**(1/n_inner) ON "// &
                         "(MOM6 av_rem/bt_rem from visc_rem"
            if (cfg%ocean%bt%strong_drag) bt_rem_msg = bt_rem_msg//", strong_drag"
            if (cfg%ocean%bt%rescale_strong_drag) bt_rem_msg = bt_rem_msg//", rescale_strong_drag"
            bt_rem_msg = bt_rem_msg//")"
            call logger%info(bt_rem_msg)
         end block
      end if

      ! MOM6 planetary-only fast loop — drop live ζ_bt/∇KE from the substeps
      ! (they stay frozen inside F_bt_*_fast); Cor_ref reduces to f·v̄.
      ocean_state%dyn%bt_work%substep_zeta_ke = cfg%ocean%bt%substep_zeta_ke
      if (compute_rank == 0 .and. .not. cfg%ocean%bt%substep_zeta_ke) then
         call logger%info("BT substep:       planetary Coriolis only "// &
                          "(substep_zeta_ke=.false.; MOM6 q=f/D parity — "// &
                          "live ζ_bt/∇KE off, frozen copies stay in F_bt)")
      end if

      ! RETIRED visc_rem-weighted BT-corrector fold (wt = visc_rem/<visc_rem>_h):
      ! `correction_visc_rem` is now fail-loud at configure (D1 follow-up —
      ! MOM6's accel_layer_u never weights this fold), so this field is
      ! wired straight from the RAW (deprecated) knob, never from
      ! `visc_rem_chain` — a test that constructs `cfg` directly and sets
      ! the raw field (bypassing the nml refusal) still gets the weighted
      ! dispatch the kernel itself supports; everyone else reads 1.
      ocean_state%dyn%bt_work%bt_correction_visc_rem = cfg%ocean%bt%correction_visc_rem
      if (compute_rank == 0 .and. cfg%ocean%bt%correction_visc_rem) then
         call logger%info("BT correction:    visc_rem/<visc_rem>_h weight ON "// &
                          "(visc_rem produced by vdiff)")
      end if
      ! D1 follow-up: the visc_rem PRODUCER is decoupled from the retired
      ! weighted fold above — it runs whenever ANY real consumer needs
      ! visc_rem_u/v fresh (forcing/renorm/bt_rem_from, or visc_rem_chain,
      ! which implies all three).
      ocean_state%dyn%bt_work%bt_visc_rem_producer = ocean_bt_visc_rem_producer_on(cfg)
      ! MOM6 wt_u parity for the BT FORCING assembly (PGF_BUG.md §9):
      ! friction-damped layers stop forcing the fast loop.  Self-sufficient
      ! (see `bt_visc_rem_producer` above).
      ocean_state%dyn%bt_work%bt_forcing_visc_rem = ocean_bt_forcing_visc_rem_on(cfg)
      if (compute_rank == 0 .and. ocean_bt_forcing_visc_rem_on(cfg)) then
         call logger%info("BT forcing:       h·visc_rem weight ON (MOM6 wt_u)")
      end if
      ! MOM6 dt·visc_rem·accel parity: the per-layer slow applies are
      ! attenuated by the viscous remnant (requires correction_visc_rem,
      ! validated at configure).
      ocean_state%dyn%accel_visc_rem = cfg%ocean%vdiff%accel_visc_rem
      if (compute_rank == 0 .and. cfg%ocean%vdiff%accel_visc_rem) then
         call logger%info("Velocity applies: visc_rem-attenuated ON "// &
                          "(MOM6 u = u0 + dt*visc_rem*accel)")
      end if
      ! Eagerly allocate the accel_visc_rem stage-entry velocity snapshots
      ! here (ocean-state convention: slots allocated at setup, mapped once
      ! at enter_data — no mid-run lazy alloc), but ONLY when the knob is on
      ! — on a large grid these two face arrays are ~GB, so the default-off
      ! path must not pay for them.  Runs BEFORE ocean_state_enter_data, so
      ! ocean_dyn_enter_data_impl copyin sees them allocated (host source=0).
      if (ocean_state%dyn%accel_visc_rem) then
         allocate (ocean_state%dyn%avr_u0(grid%nx_total + 1, grid%ny_total, &
                                          ocean_state%multilayer%nz_ml), source=0.0_wp)
         allocate (ocean_state%dyn%avr_v0(grid%nx_total, grid%ny_total + 1, &
                                          ocean_state%multilayer%nz_ml), source=0.0_wp)
      end if
      ! SPEC S3: outer split-explicit time-scheme selector.  The enum
      ! registration constrains the string (and rejects both retired
      ! spellings of this scheme, "mom6_pc" and "split_rk2", by name);
      ! the default is the MOM6 predictor-corrector, `pred_corr`.
      if (trim(cfg%ocean%bt%split_scheme) == "pred_corr") then
         ocean_state%dyn%split_scheme = SPLIT_SCHEME_PRED_CORR
      else
         ocean_state%dyn%split_scheme = SPLIT_SCHEME_SSP_RK2
      end if
      ocean_state%dyn%pc_be = cfg%ocean%bt%pc_be
      ! Print the RESOLVED scheme for BOTH branches, not just the non-default
      ! one.  A banner that only speaks up for one value cannot answer "which
      ! scheme did this namelist actually run", which is the only reliable way
      ! to enumerate what a default flip touches -- grepping the namelists
      ! cannot (a key may be absent, commented out, or set in an included
      ! group).
      if (compute_rank == 0) then
         if (ocean_state%dyn%split_scheme == SPLIT_SCHEME_PRED_CORR) then
            call logger%info("Split scheme:     pred_corr (MOM6 predictor-corrector; "// &
                             "tendencies on u_av/h_av; BE = "// &
                             to_string(cfg%ocean%bt%pc_be)//")")
         else
            call logger%info("Split scheme:     ssp_rk2 (EXPERIMENTAL; two "// &
                             "identical stages, SSP average -- grows internal "// &
                             "gravity waves from a stratified rest state)")
         end if
      end if
      ! SPEC S2b: γ-weighted continuity transport-matching inversion.
      ocean_state%dyn%bt_work%bt_renorm_visc_rem = ocean_bt_renorm_visc_rem_on(cfg)
      if (compute_rank == 0 .and. ocean_bt_renorm_visc_rem_on(cfg)) then
         call logger%info("BT renormaliser:  gamma-weighted du + u_cor (MOM6 "// &
                          "continuity inversion parity)")
      end if
      ! PR-8 / D1 follow-up: every `*_visc_rem` consumer couples against
      ! `visc_rem_u/v`, which are only ever filled away from their
      ! `source=1.0` default by the implicit bottom-drag diagonal fold
      ! (`&ocean_vdiff_nml implicit_drag`) or `bbl_glue` breaking vdiff's
      ! per-row unit-sum normalisation — without either the chain is
      ! exactly gamma==1 (a x1.0). Warn rather than abort (tidal_mixing
      ! e_uniform=0 precedent): PR-19 (visc_rem) is the named owner that
      ! fills the arrays for other configurations.  (`validate_config` also
      ! carries this check at configure; repeated here for the rank-0 log.)
      if (compute_rank == 0 .and. ocean_bt_visc_rem_producer_on(cfg) .and. &
          .not. (cfg%ocean%vdiff%implicit_drag .or. ocean_state%vdiff%bbl_glue)) then
         call logger%warning("&ocean_bt_nml forcing_visc_rem/renorm_visc_rem/"// &
                             "bt_rem_from_visc_rem/visc_rem_chain has no effect without "// &
                             "&ocean_vdiff_nml implicit_drag=.true. or bbl_glue=.true. "// &
                             "(visc_rem_u/v stay at their source=1.0 default; PR-19 owns filling them)")
      end if

      ! BT_cont_type flux-bounded continuity: allocate the per-face coefficient
      ! packs so enter_data attaches them; default off leaves them unallocated.
      ocean_state%dyn%bt_work%use_bt_cont_type = cfg%ocean%bt%use_cont_type
      if (cfg%ocean%bt%use_cont_type) then
         block
            integer :: nx_w, ny_w
            nx_w = grid%nx_total
            ny_w = grid%ny_total
            allocate (ocean_state%dyn%bt_work%BTCL_u(nx_w + 1, ny_w))
            allocate (ocean_state%dyn%bt_work%BTCL_v(nx_w, ny_w + 1))
         end block
      end if
      ! Upstream-PPM h_face for the BT chain — allocate the per-face slots.
      ocean_state%dyn%bt_work%use_upstream_h_face = cfg%ocean%bt%upstream_h_face
      if (cfg%ocean%bt%upstream_h_face) then
         block
            integer :: nx_w, ny_w
            nx_w = grid%nx_total
            ny_w = grid%ny_total
            allocate (ocean_state%dyn%bt_work%h_face_up_x(nx_w + 1, ny_w), source=0.0_wp)
            allocate (ocean_state%dyn%bt_work%h_face_up_y(nx_w, ny_w + 1), source=0.0_wp)
         end block
      end if
      if (compute_rank == 0) then
         if (ocean_state%pressure_force%variant == OPGF_VARIANT_GPRIME) then
            call logger%info("PGF form:         gprime  gfs="// &
                             to_string(cfg%ocean%pgf%gprime_gfs)//" m/s²  gint="// &
                             to_string(cfg%ocean%pgf%gprime_gint)//" m/s²")
         else
            call logger%info("PGF form:         "//trim(cfg%ocean%pgf%form))
         end if
         if (cfg%ocean%bt%bebt > 0.0_wp) then
            call logger%info("BEBT projection:  bebt="// &
                             to_string(cfg%ocean%bt%bebt)// &
                             " (BT η flux uses (1+bebt)·u^n − bebt·u^{n-1})")
         end if
         if (cfg%ocean%bt%use_cont_type) then
            call logger%info("BT_cont_type:     ON  (flux-bounded BT continuity)")
            if (cfg%ocean%bt%cont_corr_bounds) then
               call logger%info("                  BT_CONT_CORR_BOUNDS=.true. (η-corr bound from BT_cont)")
            end if
         end if
         if (cfg%ocean%bt%upstream_h_face) then
            call logger%info("BT upstream-h:    ON  (BT chain uses upstream-PPM h_face)")
         end if
      end if
   end subroutine configure_ocean_bt

   subroutine configure_ocean_bt_split(cfg, ocean_state, grid, compute_rank)
      !! Auto-derive the barotropic substep count n_inner from the external
      !! gravity-wave CFL (MOM6 set_dtbt) when requested, then latch the
      !! mode-split reference column depth bt_H_ref from the seeded bathymetry.
      !! `cfg` is intent(inout) because auto_n_inner writes cfg%ocean%bt%n_inner.
      type(config_t), intent(inout) :: cfg
      type(ocean_state_t), intent(inout) :: ocean_state
      type(hgrid_t), intent(in) :: grid
      integer, intent(in) :: compute_rank

      ! Auto-derive n_inner from the external gravity-wave CFL (MOM6 set_dtbt),
      ! evaluated PER WET CELL: the local depth with the local 2-D CFL length
      ! `l = 1/sqrt(1/dx^2+1/dy^2)` (the cross-direction term matters — on
      ! square cells it is the sqrt(2) the legacy 1-D estimate omitted), over
      ! ocean only.  See `bt_cfl_dt_wet` for why the old "deepest anywhere x
      ! smallest anywhere, land included" combination was wrong on a real
      ! global grid, and why this reproduces it bit-for-bit wherever the two
      ! extremes coincide on a wet cell.
      !
      ! `multilayer%wet_mask`, not `metrics%wet_T`: the metric mask is built
      ! later, by `configure_ocean_land_mask`; its interior IS this array.
      if (cfg%ocean%bt%auto_n_inner) then
         block
            integer :: ng_, n_inner_derived, n_wet_local
            real(wp) :: dt_bt_local, dt_bt_safe, h_at, l_at
            ng_ = grid%nghost
            call bt_cfl_dt_wet(size(ocean_state%barotropic%b, 1), &
                               size(ocean_state%barotropic%b, 2), &
                               ng_ + 1, ng_ + grid%nx_phys, &
                               ng_ + 1, ng_ + grid%ny_phys, &
                               ocean_state%barotropic%b, &
                               ocean_state%multilayer%wet_mask, &
                               ocean_state%metrics%dxT, ocean_state%metrics%dyT, &
                               cfg%ocean%bt%cfl_bt_safety, &
                               dt_bt_local, h_at, l_at, n_wet_local)
            ! Reduce to the GLOBAL per-point minimum so every rank derives the
            ! SAME n_inner.  Different n_inner per rank means a different
            ! number of barotropic substeps, which desyncs the per-substep
            ! grouped halo exchanges (Isend/Irecv pairing crosses between
            ! substeps -> MPI_ERR_TRUNCATE at the first N/S exchange; seen on
            ! a spherical py>1 split).  A rank with no wet cell contributes
            ! `huge`, the identity of min.  Single rank: identity stub.
            call halo_allreduce_min(dt_bt_local, dt_bt_safe)
            n_inner_derived = bt_auto_n_inner_from_dt(cfg%dt_fixed, dt_bt_safe)
            if (compute_rank == 0) then
               if (dt_bt_safe >= huge(1.0_wp)) then
                  call logger%warning("Auto n_inner: no wet cell anywhere — "// &
                                      "n_inner = 1 (was "// &
                                      to_string(cfg%ocean%bt%n_inner)//")")
               else if (dt_bt_local == dt_bt_safe) then
                  call logger%info("Auto n_inner (per wet cell): limiting cell H = "// &
                                   to_string(h_at)//" m, l_cfl = "// &
                                   to_string(l_at)//" m, c_ext = "// &
                                   to_string(sqrt(GRAVITY*max(h_at, 1.0_wp)))// &
                                   " m/s, dt_bt = "//to_string(dt_bt_safe)// &
                                   " s → n_inner = "//to_string(n_inner_derived)// &
                                   " (was "//to_string(cfg%ocean%bt%n_inner)//")")
               else
                  call logger%info("Auto n_inner (per wet cell): dt_bt = "// &
                                   to_string(dt_bt_safe)//" s (limited on another "// &
                                   "rank) → n_inner = "//to_string(n_inner_derived)// &
                                   " (was "//to_string(cfg%ocean%bt%n_inner)//")")
               end if
            end if
            cfg%ocean%bt%n_inner = n_inner_derived
         end block
      end if

      ! Mode-split contract: bt_H_ref is the reference WATER-COLUMN
      ! thickness.  Without an ice shelf that is the seeded bathymetry
      ! (Σh_layer = b); under one it is `b − z_draft`, which is what makes
      ! `bt_eta = Σh_layer − bt_H_ref` the deviation from the LOADED
      ! equilibrium (zero at rest) rather than a permanent −z_draft.  That
      ! is Losch (2008) §2.1's own convention, and it is why every
      ! consumer of `D = bt_H_ref + bt_eta` — the BT continuity face
      ! thickness, the Chapman phase speed, the ALE `remap_h_ref` — needs
      ! no cavity branch of its own.  Knob off ⇒ `z_draft ≡ 0` ⇒ the
      ! literal `bt_H_ref = b` this always was.
      !
      ! The snapshot is taken from the UNWRAPPED `b`/`z_draft`; the engine
      ! re-wraps + halo-exchanges `bt_H_ref` right after, alongside both
      ! of its sources.
      !
      ! NOTE on `auto_n_inner` above: it derives each wet cell's external
      ! gravity-wave speed from `b`, the BED depth, not from `b − z_draft`.
      ! Under a shelf that OVERESTIMATES `c_ext` and so buys more
      ! barotropic substeps than the CFL needs — conservative, never
      ! unstable, and at a calving front (where the draft is 0) it is
      ! exactly right.  Left as-is deliberately: tightening it would
      ! change `n_inner` for cavity runs only, which is a separate,
      ! answer-changing decision.
      if (cfg%ocean%bt%n_inner >= 1) then
         if (ocean_state%metrics%use_cavity) then
            ! `cavity_datum_impl`, not the raw `b - z_draft`: a GROUNDED
            ! column has no water column, and its datum is 0 rather than
            ! a negative thickness.  See that routine for why — in short,
            ! it is land, nothing downstream reads its datum, and the
            ! negative value put a phantom few-hundred-metre `bt_eta` on
            ! it and turned the ALE land target into a cancellation.
            call cavity_datum_impl(ocean_state%dyn%bt_work%bt_H_ref, &
                                   ocean_state%barotropic%b, &
                                   ocean_state%metrics%z_draft, &
                                   cfg%ocean%cavity_dyn%h_min_cavity, &
                                   size(ocean_state%barotropic%b, 1), &
                                   size(ocean_state%barotropic%b, 2))
         else
            ocean_state%dyn%bt_work%bt_H_ref = ocean_state%barotropic%b
         end if
      end if
   end subroutine configure_ocean_bt_split

   subroutine configure_ocean_cavity(cfg, ocean_state, grid, compute_rank, ierr)
      !! Build the static ice-shelf cavity LOAD field
      !! `metrics%p_ice_ref = (rho_ref*GRAVITY)*z_draft` (Pa), ASSEMBLE it
      !! into the top-of-column pressure `multilayer_state_t%p_top`, and
      !! assert the counted-once datum invariant.
      !!
      !! The GEOMETRY (`z_draft`, `cover_frac`) is filled much earlier, in
      !! `ocean_state_seed_from_cfg`, because the wet mask and the layer
      !! split are seeded from `b − z_draft`.  What is left for configure
      !! is the part that needs the PGF's reference density, which
      !! `configure_ocean_pgf` / `configure_ocean_reference_density` only
      !! settle later — hence this runs after them and before
      !! `ocean_state_enter_data`, like every other static field the
      !! device map has to capture.
      !!
      !! `rho_ref*GRAVITY` is formed as ONE product, the same one the
      !! FV_MOM6 Pass-1 surface BC forms, so that
      !! `pa(nz+1) = rho_ref*g*(−z_draft) + p_ice_ref` cancels to bit-zero
      !! at rest (exactly when the toolchain rounds the product before the
      !! add; under FMA contraction, to the rounding of `p_ice_ref`).
      !!
      !! ### The partition (P5.2)
      !!
      !! ```
      !! ms%p_top = metrics%p_ice_ref  +  sf%p_surf
      !!            (static ice load)     (atmospheric / anomaly load)
      !! ```
      !!
      !! `p_ice_ref` goes HERE and to the datum (`bt_H_ref = b − z_draft`)
      !! and NOWHERE else — in particular it is never added into
      !! `sf%p_surf`, because `eta_ib = −p_surf/(ρ₀ g_bt)` is built from
      !! the assembled total and the datum already carries exactly this
      !! much.  Only the load ANOMALY reaches the `eta_forcing` seam, and
      !! for the Boussinesq-isostatic default that anomaly IS `sf%p_surf`
      !! (a cavity with no atmospheric load sends the seam nothing at
      !! all).  See `src/core/ocean/README.md`'s `p_top` / `eta_forcing`
      !! seam contracts.
      !!
      !! This seed is the FINAL value for a cavity without the psurf seam
      !! (the draft is static, so there is nothing to refresh); with psurf
      !! enabled, `ocean_dyn_step_split` rebuilds the same sum once per
      !! outer step from the live `sf%p_surf`.  Host-side plain `do`
      !! loops, before `ocean_state_enter_data` maps the result.
      type(config_t), intent(in) :: cfg
      type(ocean_state_t), intent(inout) :: ocean_state
      type(hgrid_t), intent(in) :: grid
      integer, intent(in) :: compute_rank
      integer, intent(out), optional :: ierr
         !! Non-zero on a cavity configuration conflict when present;
         !! absent behaves as today (`error stop`).

      integer :: nx, ny, i, j
      real(wp) :: resid, datum_tol

      if (.not. ocean_state%metrics%use_cavity) then
         if (present(ierr)) ierr = OCEAN_STATUS_OK
         return
      end if

      nx = size(ocean_state%metrics%z_draft, 1)
      ny = size(ocean_state%metrics%z_draft, 2)

      call cavity_fill_p_ice_ref(ocean_state%metrics%p_ice_ref, &
                                 ocean_state%metrics%z_draft, &
                                 ocean_state%pressure_force%rho_ref*GRAVITY, nx, ny)

      ! P5.2 — THE LOAD MUST HAVE A CONSUMER WHEN IT HAS A GRADIENT.
      !
      ! `&ocean_pgf_nml p_top_in_bc` is the only route by which a cavity
      ! load reaches the pressure stack, and a cavity whose draft VARIES
      ! is not in hydrostatic balance without it: `pa(nz+1)` then carries
      ! the uncancelled `-rho_ref*g*z_draft`, so the stack sits ~5e6 Pa
      ! off its anomaly scale (every `h_neglect` face-divisor leak grows
      ! by the same factor), the UNSPLIT driver — which has no
      ! depth-mean replacement — feels a raw `g*grad(z_draft)` ~ 0.1 m/s^2,
      ! and a non-uniform barotropic-correction weight turns the
      ! uncancelled depth-uniform force into a real per-layer shear.
      ! Refused, not auto-enabled: the namelist should say what the run does.
      !
      ! EXEMPTION, and it is a theorem rather than a courtesy: a draft
      ! that is UNIFORM over the whole array has a load with no gradient,
      ! and a gradient-free `p_top` is bit-identically inert in the top BC
      ! (`test_ocean_pgf_p_top_bc::uniform_p_top_bit_identical`).  Such a
      ! run — the flat-lid datum-equivalence case — is refused by nothing.
      !
      ! Tested on the FILLED array, not on the namelist shape: land
      ! exclusion can zero `z_draft` under a formula that looks uniform.
      ! `validate_config` carries the same refusal on the config-level
      ! predicate so the user meets it before any state is built.
      if (.not. ocean_state%pressure_force%p_top_in_bc) then
         if (maxval(ocean_state%metrics%z_draft) /= &
             minval(ocean_state%metrics%z_draft)) then
            call fail("&ocean_cavity_dyn_nml enable=.true. with a NON-UNIFORM "// &
                      "draft requires &ocean_pgf_nml p_top_in_bc=.true.  The "// &
                      "isostatic load rho_ref*g*z_draft would otherwise never "// &
                      "reach the FV_MOM6 pa(nz+1) surface boundary condition, "// &
                      "leaving the pressure stack ~"// &
                      to_string(maxval(ocean_state%metrics%p_ice_ref))// &
                      " Pa off its anomaly scale and the column out of "// &
                      "hydrostatic balance.  (A UNIFORM draft is exempt: a "// &
                      "load with no gradient is provably inert in the top BC.)", &
                      ierr, OCEAN_STATUS_ERR_SETUP)
            return
         end if
      end if

      ! P5.2: assemble the top-of-column load.  Rebuilt from scratch (not
      ! `+=` onto whatever the earlier psurf seed left) so the result does
      ! not depend on which of the two seeds ran first, and over the WHOLE
      ! array including ghosts — `p_top` owes no halo exchange of its own
      ! precisely because both of its sources are already ghost-valid.
      if (.not. (size(ocean_state%multilayer%p_top, 1) == nx .and. &
                 size(ocean_state%multilayer%p_top, 2) == ny)) then
         call fail("configure_ocean_cavity: ms%p_top and metrics%p_ice_ref "// &
                   "have different shapes — the top-of-column load cannot be "// &
                   "assembled.", ierr, OCEAN_STATUS_ERR_SETUP)
         return
      end if
      do j = 1, ny
         do i = 1, nx
            ocean_state%multilayer%p_top(i, j) = ocean_state%metrics%p_ice_ref(i, j)
         end do
      end do
      if (allocated(ocean_state%surface_flux%p_surf)) then
         do j = 1, ny
            do i = 1, nx
               ocean_state%multilayer%p_top(i, j) = &
                  ocean_state%multilayer%p_top(i, j) + &
                  ocean_state%surface_flux%p_surf(i, j)
            end do
         end do
      end if

      ! ---- The vertical coordinate's rigid-top seam (P6.2) ----
      ! `vcoord%z_top(i,j)` is the geopotential depth of the top of the
      ! WATER column — the ice base under a shelf, `z = 0` elsewhere.
      ! The draft is static, so this is a configure-time copy and the
      ! vcoord slot stays self-contained at run time: the ALE remap
      ! driver never sees `metrics` and the target builder's signature is
      ! unchanged.  Same pattern (and the same reason) as the sponge's
      ! `sp%z_top` in `configure_ocean_sponge`.
      !
      ! Consumed by the `VCOORD_Z_FIXED` branch of `compute_target_h`.
      ! Without a cavity this routine has already returned, so `z_top`
      ! keeps its init-time zero fill and every geometric family
      ! reproduces its pre-cavity arithmetic bit-for-bit.  BEFORE
      ! `ocean_state_enter_data` maps it.
      if (allocated(ocean_state%vcoord%z_top)) then
         if (size(ocean_state%vcoord%z_top, 1) == nx .and. &
             size(ocean_state%vcoord%z_top, 2) == ny) then
            do j = 1, ny
               do i = 1, nx
                  ocean_state%vcoord%z_top(i, j) = ocean_state%metrics%z_draft(i, j)
               end do
            end do
         else
            call fail("configure_ocean_cavity: vcoord%z_top and metrics%z_draft "// &
                      "have different shapes — the vertical coordinate cannot see "// &
                      "the ice base.", ierr, OCEAN_STATUS_ERR_SETUP)
            return
         end if
      end if

      ! The counted-once invariant (I), in metres of reference depth, over
      ! the WET columns:
      !     rho*g*z_draft + (bt_H_ref - b)*rho*g == 0   <=>   bt_H_ref == b - z_draft.
      ! GROUNDED columns are excluded because they are LAND: every face
      ! metric on them is zero, so they carry no barotropic momentum
      ! equation and there is no load on them to count once or twice.
      ! Their datum is deliberately 0 (`cavity_datum_impl`).
      ! Asserted on the common positive factor divided out — scale-free,
      ! and it does not fabricate a product the code never forms.  The
      ! bound is a pure round-off allowance on the ONE subtraction
      ! `b - z_draft`: both operands are O(max depth), so the result
      ! carries at most a few ulp of it.  (It is NOT asserted as bit-zero:
      ! the latch and this check evaluate the same difference in two
      ! places, and an FMA-contracting build is free to round them
      ! differently.)
      if (cfg%ocean%bt%n_inner >= 1) then
         datum_tol = 8.0_wp*epsilon(1.0_wp)* &
                     max(maxval(abs(ocean_state%barotropic%b)), 1.0_wp)
         resid = cavity_datum_residual(ocean_state%dyn%bt_work%bt_H_ref, &
                                       ocean_state%barotropic%b, &
                                       ocean_state%metrics%z_draft, &
                                       cfg%ocean%cavity_dyn%h_min_cavity, nx, ny)
         if (.not. (resid <= datum_tol)) then
            call fail("&ocean_cavity_dyn_nml: the barotropic datum and the ice "// &
                      "draft disagree (max |bt_H_ref - (b - z_draft)| = "// &
                      to_string(resid)//" m > "//to_string(datum_tol)//" m).  The "// &
                      "ice load would then be counted twice, or not at all — check "// &
                      "that z_draft went through the same periodic/fold re-wrap and "// &
                      "halo exchange as b.", ierr, OCEAN_STATUS_ERR_SETUP)
            return
         end if
      end if

      ! ---- Cover mask on the atmospheric forcing (P2c) ----
      ! There is no atmosphere under an ice shelf.  Two static forcing
      ! fields are masked ONCE, here, because this is the first point at
      ! which `cover_frac` exists AND the forcing has been seeded
      ! (`configure_ocean_forcing` runs much earlier, before the cavity
      ! geometry):
      !
      !   * the wind-stress PAIR, masked on every face touching a covered
      !     cell and followed by the `stress_mag` refresh in the same
      !     call — see `ocean_surface_stress_apply_cover`.  Masking the
      !     source rather than the derived views is what also silences
      !     the implicit vdiff stress fold and the MLE front sampler,
      !     which read `ss%tau_x` raw;
      !   * the SCALAR `&ocean_thermo_nml q_heat` / `q_salt` fill, but
      !     ONLY when the component set is off.  With components on those
      !     two are assembler outputs and the mask belongs in
      !     `ocean_surface_flux_assemble` (a second writer here would
      !     break the fill contract); the call below no-ops itself in
      !     that case.
      !
      ! Both are host-side and both run BEFORE `ocean_state_enter_data`,
      ! so the masked values are what the device map captures.  Both are
      ! idempotent.  The time-varying twin of the wind mask lives in
      ! `ocean_seam_refresh_surface_stress` (per data-forcing bracket).
      call ocean_surface_stress_apply_cover(ocean_state%surface_stress, &
                                            ocean_state%metrics%cover_frac)
      call ocean_surface_flux_apply_cover_const(ocean_state%surface_flux, &
                                                ocean_state%metrics%cover_frac)

      if (compute_rank == 0) then
         call logger%info("Ice-shelf cavity: isostatic load p_ice_ref = "// &
                          "rho_ref*g*z_draft, max = "// &
                          to_string(maxval(ocean_state%metrics%p_ice_ref))// &
                          " Pa (rho_ref = "// &
                          to_string(ocean_state%pressure_force%rho_ref)//" kg/m^3)")
         call logger%info("                  assembled into ms%p_top (max = "// &
                          to_string(maxval(ocean_state%multilayer%p_top))// &
                          " Pa = p_ice_ref + sf%p_surf); NOT into sf%p_surf, "// &
                          "which the eta_forcing seam is built from")
         if (ocean_state%pressure_force%p_top_in_bc) then
            call logger%info("                  consumed by the FV_MOM6 pa(nz+1) "// &
                             "surface BC (&ocean_pgf_nml p_top_in_bc)")
         else
            call logger%info("                  the draft is UNIFORM here, so the "// &
                             "load has no gradient and the PGF top BC is provably "// &
                             "inert; &ocean_pgf_nml p_top_in_bc is not required")
         end if
      end if
      if (present(ierr)) ierr = OCEAN_STATUS_OK
   end subroutine configure_ocean_cavity

   subroutine configure_ocean_cavity_melt(cfg, ocean_state, grid, compute_rank, ierr)
      !! Configure the ice-shelf basal-melt slot (`&ocean_cavity_melt_nml`,
      !! P2b): copy the knobs onto the slot's three flat parameter
      !! bundles, resolve the `gamma_s` sentinel, build the per-column
      !! Coriolis array the `hj99` law needs, and CHECK that the
      !! interface pressure the liquidus will read has actually been
      !! loaded.
      !!
      !! Runs immediately after `configure_ocean_cavity` — which is the
      !! SOLE producer of `ms%p_top` — and before
      !! `ocean_state_enter_data`, like every other static fill the
      !! device map has to capture.
      !!
      !! **`ms%p_top`: this routine is a CONSUMER, not a writer.**
      !! `ms%p_top` is THE interface pressure
      !! (`src/core/ocean/README.md`, the `p_top` seam contract), and the
      !! melt liquidus is its THIRD consumer alongside the FV_MOM6
      !! `pa(nz+1)` surface boundary condition and the in-situ EOS.  It
      !! has exactly ONE producer, `configure_ocean_cavity`, which
      !! assembles `ms%p_top = metrics%p_ice_ref + sf%p_surf` (P5.2) —
      !! rebuilt per outer step in `ocean_dyn_step_split` when the psurf
      !! seam makes `sf%p_surf` live.  A second writer here would be a
      !! silent clobber, so there is none: what this routine does instead
      !! is ASSERT that every ice-covered column carries at least its own
      !! isostatic load, which catches a producer that was skipped or
      !! reordered rather than trusting the call order.
      use rdb_ocean_cavity_melt, only: parse_cavity_exchange_law, parse_cavity_ice_mode, &
                                       CAVITY_LAW_HJ99, parse_cavity_freshwater, &
                                       parse_cavity_volume_comp, CAVITY_FW_MASS, &
                                       CAVITY_VC_UNIFORM_OPEN
      type(config_t), intent(in) :: cfg
      type(ocean_state_t), intent(inout) :: ocean_state
      type(hgrid_t), intent(in) :: grid
      integer, intent(in) :: compute_rank
      integer, intent(out), optional :: ierr
         !! Non-zero on a melt configuration conflict when present;
         !! absent behaves as today (`error stop`).
      integer :: nx, ny, n_zero_f, n_unloaded
      real(wp) :: gamma_s_eff

      if (.not. ocean_state%cavity_flux%enable) then
         if (present(ierr)) ierr = OCEAN_STATUS_OK
         return
      end if

      nx = size(ocean_state%cavity_flux%f_cor, 1)
      ny = size(ocean_state%cavity_flux%f_cor, 2)

      ! ---- knobs -> slot ----
      ocean_state%cavity_flux%far_field_depth = cfg%ocean%cavity_melt%far_field_depth
      ocean_state%cavity_flux%cdrag_top = cfg%ocean%cavity_melt%cdrag_top
      ocean_state%cavity_flux%u_tide = cfg%ocean%cavity_melt%u_tide
      ocean_state%cavity_flux%ustar_min = cfg%ocean%cavity_melt%ustar_min
      ocean_state%cavity_flux%s_ice = cfg%ocean%cavity_melt%s_ice
      ocean_state%cavity_flux%freshwater = &
         parse_cavity_freshwater(cfg%ocean%cavity_melt%freshwater)
      ocean_state%cavity_flux%volume_comp = &
         parse_cavity_volume_comp(cfg%ocean%cavity_melt%volume_compensation)

      ! THE Boussinesq reference density, taken from the surface-flux
      ! slot rather than re-read from `cfg`, because the real-mass path's
      ! salt correction has to be the EXACT negation of the increment
      ! `apply_surface_src_2d_impl` stamped with `dt/sf%rho0`.  One
      ! source, no second literal, and `configure_ocean_reference_density`
      ! has already run (it seeds `sf%rho0` from `&ocean_ic_nml rho_0`).
      ocean_state%cavity_flux%rho0 = ocean_state%surface_flux%rho0
      if (ocean_state%cavity_flux%rho0 <= 0.0_wp) then
         call fail("&ocean_cavity_melt_nml: the Boussinesq reference density "// &
                   "reaching the melt slot is non-positive, so the mass -> volume "// &
                   "conversion m/rho_0 has no value.  configure_ocean_reference_"// &
                   "density must run before configure_ocean_cavity_melt.", ierr, &
                   OCEAN_STATUS_ERR_SETUP)
         return
      end if

      ! `gamma_s` carries the repo's negative "unset" sentinel; resolve
      ! it to the ISOMIP+ `gamma_t/35` exactly once, here, so the value
      ! the kernel is handed is the value the log prints.
      gamma_s_eff = cavity_resolve_gamma_s(cfg%ocean%cavity_melt%gamma_s, &
                                           cfg%ocean%cavity_melt%gamma_t)
      ocean_state%cavity_flux%par%law = &
         parse_cavity_exchange_law(cfg%ocean%cavity_melt%exchange_law)
      ocean_state%cavity_flux%par%gamma_t_coeff = cfg%ocean%cavity_melt%gamma_t
      ocean_state%cavity_flux%par%gamma_s_coeff = gamma_s_eff
      ocean_state%cavity_flux%ice%mode = &
         parse_cavity_ice_mode(cfg%ocean%cavity_melt%ice_conduction)
      ocean_state%cavity_flux%ice%T_ice = cfg%ocean%cavity_melt%t_ice

      ! `cavity_flux%const` is deliberately LEFT at its ISOMIP+ defaults
      ! (Asay-Davis et al. (2016) Table 4): `rho_w`, `c_w`, `alpha_T`,
      ! `beta_S` there are the melt law's own calibrated constants, not
      ! copies of the Boussinesq `rho_0` — the same "out of scope on
      ! purpose" category the README's reference-density table already
      ! lists `RHO_WATER` and `&ocean_ice_nml rho_ocean` under.
      ! Overriding them would silently break ISOMIP+ comparability,
      ! which is the reason this path exists.

      ! ---- per-column Coriolis for the hj99 law ----
      call fill_coriolis_centre(cfg, ocean_state%metrics, grid, ocean_state%cavity_flux%f_cor)
      if (ocean_state%cavity_flux%par%law == CAVITY_LAW_HJ99) then
         n_zero_f = cavity_count_zero_f(ocean_state%cavity_flux%f_cor, &
                                        ocean_state%metrics%cover_frac, &
                                        ocean_state%multilayer%wet_mask, nx, ny)
         if (n_zero_f > 0) then
            call fail("&ocean_cavity_melt_nml exchange_law='hj99': "// &
                      to_string(n_zero_f)//" ice-covered wet column(s) sit at "// &
                      "f = 0.  Holland & Jenkins (1999) eq. (15) takes "// &
                      "ln(.../|f| h_nu) and eq. (18) divides by f*L_O, so the law "// &
                      "has no value there and the kernel would return "// &
                      "CAVITY_MELT_NO_CORIOLIS for every one of them.  Use "// &
                      "exchange_law='const_gamma', or move the cavity off the "// &
                      "f = 0 line.", ierr, OCEAN_STATUS_ERR_SETUP)
            return
         end if
      end if

      ! ---- the interface pressure: ASSERT, never write (see the docstring) ----
      ! `p_surf >= 0` by contract, so a correctly assembled `p_top` is
      ! `>= p_ice_ref` on every cell.  A zero `p_top` under a loaded
      ! draft means the producer never ran (or ran before the load was
      ! built), which would melt against a surface-pressure liquidus and
      ! silently delete the entire ice pump.
      n_unloaded = cavity_count_unloaded_p_top(ocean_state%multilayer%p_top, &
                                               ocean_state%metrics%p_ice_ref, nx, ny)
      if (n_unloaded > 0) then
         call fail("&ocean_cavity_melt_nml: "//to_string(n_unloaded)//" column(s) "// &
                   "have ms%p_top < metrics%p_ice_ref, so the top-of-column load was "// &
                   "never assembled.  The melt liquidus is a CONSUMER of ms%p_top; "// &
                   "its sole producer is configure_ocean_cavity "// &
                   "(p_top = p_ice_ref + sf%p_surf), which must run before this "// &
                   "routine.", ierr, OCEAN_STATUS_ERR_SETUP)
         return
      end if

      if (compute_rank == 0) then
         call logger%info("Ice-shelf basal melt: exchange_law='"// &
                          trim(adjustl(cfg%ocean%cavity_melt%exchange_law))// &
                          "', Gamma_T = "//to_string(cfg%ocean%cavity_melt%gamma_t)// &
                          ", Gamma_S = "//to_string(gamma_s_eff)// &
                          ", ice='"// &
                          trim(adjustl(cfg%ocean%cavity_melt%ice_conduction))//"'")
         call logger%info("                      far_field_depth = "// &
                          to_string(cfg%ocean%cavity_melt%far_field_depth)// &
                          " m (METRES below the ice base, not 'layer nz'), "// &
                          "C_d,top = "//to_string(cfg%ocean%cavity_melt%cdrag_top)// &
                          ", u_tide = "//to_string(cfg%ocean%cavity_melt%u_tide)//" m/s")
         call logger%info("                      liquidus evaluated at ms%p_top "// &
                          "(max = "// &
                          to_string(maxval(ocean_state%multilayer%p_top))// &
                          " Pa), assembled by configure_ocean_cavity as "// &
                          "p_ice_ref + sf%p_surf")
         if (ocean_state%cavity_flux%freshwater == CAVITY_FW_MASS) then
            call logger%info("                      freshwater = 'mass': the "// &
                             "meltwater is a REAL Boussinesq volume on the top "// &
                             "layer, dh = m*dt/rho_0 with rho_0 = "// &
                             to_string(ocean_state%cavity_flux%rho0)//" kg/m3.  The "// &
                             "virtual salt flux stays in Q_salt as the KPP/EPBL B_0 "// &
                             "buoyancy forcing and is removed again from the tracer "// &
                             "in the same stage.")
            if (ocean_state%cavity_flux%volume_comp == CAVITY_VC_UNIFORM_OPEN) then
               call logger%info("                      volume_compensation = "// &
                                "'uniform_open_ocean': the domain-integrated melt "// &
                                "volume is removed again each thermo step over the "// &
                                "uncovered wet cells, carrying their own T and S, "// &
                                "and is tracked as a sink in all three budgets.")
            else
               call logger%warning("Ice-shelf basal melt: freshwater='mass' with "// &
                                   "volume_compensation='none' — a CLOSED domain "// &
                                   "gains the melt volume and its sea level rises "// &
                                   "(for an ISOMIP+ Ocean0 box that is metres per "// &
                                   "year).  Correct for a short run or an open "// &
                                   "boundary; otherwise set "// &
                                   "volume_compensation='uniform_open_ocean'.")
            end if
         else
            call logger%warning("Ice-shelf basal melt: the meltwater is a VIRTUAL SALT "// &
                                "FLUX — it carries no mass, so it adds no volume and no "// &
                                "direct buoyancy.  That is a first-order limitation for "// &
                                "cavity circulation; set &ocean_cavity_melt_nml "// &
                                "freshwater='mass' for the real volume source.")
         end if
      end if
      if (present(ierr)) ierr = OCEAN_STATUS_OK
   end subroutine configure_ocean_cavity_melt

   subroutine configure_ocean_top_drag(cfg, ocean_state, grid, compute_rank)
      !! Ice-shelf TOP drag (`&ocean_tdrag_nml`, Phase 4a): copy the
      !! variant + coefficients onto the slot, project the static
      !! cell-centred `metrics%cover_frac` onto the velocity FACES, and
      !! enforce the ONE-`C_d` rule against the melt slot.
      !!
      !! Runs AFTER `configure_ocean_cavity_melt` (which is where the melt
      !! slot's `cdrag_top` is seeded, and which this routine then
      !! overwrites from `&ocean_tdrag_nml cd` — `validate_config` has
      !! already refused a disagreement, so the assignment is structural,
      !! not a silent override) and AFTER the `cover_frac` halo exchange,
      !! and BEFORE `ocean_state_enter_data` so the host-filled face masks
      !! reach the device with the `copyin` map.
      !!
      !! Disabled ⇒ nothing is written, the slot keeps its placeholder
      !! arrays, and the run is bit-identical.
      type(config_t), intent(in) :: cfg
      type(ocean_state_t), intent(inout) :: ocean_state
      type(hgrid_t), intent(in) :: grid
      integer, intent(in) :: compute_rank

      integer :: nx, ny

      if (.not. cfg%ocean%tdrag%enable) return

      nx = grid%nx_total
      ny = grid%ny_total

      ocean_state%tdrag%variant = parse_tdrag_variant(cfg%ocean%tdrag%form)
      ocean_state%tdrag%c_drag = cfg%ocean%tdrag%cd
      ocean_state%tdrag%r_linear = cfg%ocean%tdrag%r
      ocean_state%tdrag%htbl = cfg%ocean%tdrag%htbl
      ocean_state%tdrag%drag_bg_vel = cfg%ocean%tdrag%bg_vel
      ocean_state%tdrag%tbl_thick_min = cfg%ocean%tdrag%tbl_thick_min
      ocean_state%tdrag%implicit = cfg%ocean%tdrag%implicit
      ! `&ocean_vdiff_nml implicit_top_drag` is the OTHER implicit form:
      ! the fold into the vdiff `k = nz` diagonal.  Latched on both slots
      ! — the top-drag kernel needs it to fill `lambda_top_u/v`, and the
      ! driver reads `td%implicit_fold` to skip the explicit apply.
      ! `validate_config` has already refused the double-count
      ! (`implicit_top_drag` × `&ocean_tdrag_nml implicit`) and the
      ! distributed case (`htbl > 0`).
      ocean_state%tdrag%implicit_fold = cfg%ocean%vdiff%implicit_top_drag
      ocean_state%vdiff%implicit_top_drag = cfg%ocean%vdiff%implicit_top_drag
      ! `rho0` is only ever used to scale the `stress_top` diagnostic into
      ! N/m^2; no dynamics reads it.  The one rho0 of record is
      ! `&ocean_ic_nml rho_0` -> `eos%rho0`, already resolved by
      ! `configure_ocean_reference_density` well before this point.
      ocean_state%tdrag%rho0 = ocean_state%eos%rho0

      ! Static face cover, OR of the two abutting cells (see
      ! `rdb_ocean_top_drag`'s module docstring for why OR at a calving
      ! front), plus the cell-centred copy the `stress_top` diagnostic
      ! reads.  `cover_frac` is `&ocean_cavity_dyn_nml` geometry: filled
      ! in the IC seed, halo-exchanged by the engine, never touched again.
      call top_drag_fill_face_cover_impl(ocean_state%tdrag%cover_u, &
                                         ocean_state%tdrag%cover_v, &
                                         ocean_state%metrics%cover_frac, nx, ny)
      ocean_state%tdrag%cover_t(:, :) = ocean_state%metrics%cover_frac(:, :)

      ! ONE drag coefficient for momentum and melt.  MOM6 carries two
      ! independent top-drag coefficients; we deliberately do not — see
      ! the agreement rule in `validate_config`, which has already failed
      ! loud if the user set two different values.
      if (cfg%ocean%cavity_melt%enable .and. &
          ocean_state%tdrag%variant == TDRAG_QUADRATIC) then
         ocean_state%cavity_flux%cdrag_top = cfg%ocean%tdrag%cd
      end if

      if (compute_rank == 0) then
         call logger%info("Ice-shelf top drag: "//trim(cfg%ocean%tdrag%form)// &
                          "  C_d="//to_string(cfg%ocean%tdrag%cd)// &
                          "  r="//to_string(cfg%ocean%tdrag%r)//" 1/s")
         if (cfg%ocean%tdrag%htbl > 0.0_wp) then
            call logger%info("  top BL:         HTBL="//to_string(cfg%ocean%tdrag%htbl)// &
                             " m  bg_vel="//to_string(cfg%ocean%tdrag%bg_vel)// &
                             " m/s  thick_min="// &
                             to_string(cfg%ocean%tdrag%tbl_thick_min)//" m")
         else
            call logger%info("  top BL:         layer-nz only (HTBL=0)")
         end if
         call logger%info("  covered faces:  u "// &
                          to_string(int(sum(ocean_state%tdrag%cover_u)))//", v "// &
                          to_string(int(sum(ocean_state%tdrag%cover_v)))// &
                          " (OR of the two abutting cells)")
         if (cfg%ocean%cavity_melt%enable) then
            call logger%info("  melt u* C_d:    taken from &ocean_tdrag_nml cd "// &
                             "(one coefficient for momentum and melt)")
         end if
      end if
   end subroutine configure_ocean_top_drag

   pure function cavity_resolve_gamma_s(gamma_s, gamma_t) result(gamma_s_eff)
      !! Resolve the `&ocean_cavity_melt_nml gamma_s` "unset" sentinel to
      !! the ISOMIP+ default `gamma_t/CAVITY_GAMMA_RATIO_ISOMIP`
      !! (Asay-Davis et al. (2016) Table 4 p. 2483; the 35 is Jenkins,
      !! Nicholls & Corr (2010) p. 2309).
      !!
      !! A NEGATIVE `gamma_s` means "not set by the user"; zero and
      !! positive values are taken literally — zero is then refused as a
      !! range error by `validate_config` rather than silently
      !! re-triggering the default, because the three-equation form
      !! divides by `gamma_s`.
      real(wp), intent(in) :: gamma_s
         !! The raw knob; negative = unset sentinel.
      real(wp), intent(in) :: gamma_t
         !! Heat-transfer coefficient the default is a fraction of.
      real(wp) :: gamma_s_eff
      gamma_s_eff = merge(gamma_t/CAVITY_GAMMA_RATIO_ISOMIP, gamma_s, gamma_s < 0.0_wp)
   end function cavity_resolve_gamma_s

   pure function cavity_count_zero_f(f_cor, cover_frac, wet_mask, nx, ny) result(n_zero)
      !! Count ice-covered WET columns sitting at exactly `f = 0` — the
      !! configure-time domain check for `exchange_law = "hj99"`.
      !!
      !! Exactly zero, not "small": the law's failure at `f = 0` is a
      !! division and a logarithm, not a loss of accuracy, and a small
      !! `|f|` is a legitimate (if strongly suppressed) answer.
      integer, intent(in) :: nx
         !! First dimension.
      integer, intent(in) :: ny
         !! Second dimension.
      real(wp), intent(in) :: f_cor(nx, ny)
         !! Cell-centred Coriolis parameter (1/s).
      real(wp), intent(in) :: cover_frac(nx, ny)
         !! Ice-cover fraction.
      real(wp), intent(in) :: wet_mask(nx, ny)
         !! Static wet/land mask.
      integer :: n_zero
      integer :: i, j
      n_zero = 0
      do j = 1, ny
         do i = 1, nx
            if (cover_frac(i, j) > 0.5_wp .and. wet_mask(i, j) > 0.5_wp) then
               if (f_cor(i, j) == 0.0_wp) n_zero = n_zero + 1
            end if
         end do
      end do
   end function cavity_count_zero_f

   pure function cavity_count_unloaded_p_top(p_top, p_ice_ref, nx, ny) result(n_unloaded)
      !! Count cells whose top-of-column pressure does NOT carry the
      !! isostatic ice load — the melt path's guard that
      !! `configure_ocean_cavity` (the sole `ms%p_top` producer) ran, and
      !! ran before this check.
      !!
      !! The test is `p_top < p_ice_ref`, which is exact rather than a
      !! tolerance: `ms%p_top = p_ice_ref + sf%p_surf` with
      !! `sf%p_surf >= 0` by contract, so a loaded column satisfies it
      !! with no rounding argument at all, and an unloaded one misses it
      !! by the whole 5e6 Pa.  A `pure` predicate, so the refusal can be
      !! tested without provoking the `error stop`.
      integer, intent(in) :: nx
         !! First dimension.
      integer, intent(in) :: ny
         !! Second dimension.
      real(wp), intent(in) :: p_top(nx, ny)
         !! `multilayer_state_t%p_top` (Pa).
      real(wp), intent(in) :: p_ice_ref(nx, ny)
         !! `metrics%p_ice_ref` (Pa) = `rho_ref*GRAVITY*z_draft`.
      integer :: n_unloaded
      integer :: i, j
      n_unloaded = 0
      do j = 1, ny
         do i = 1, nx
            if (p_top(i, j) < p_ice_ref(i, j)) n_unloaded = n_unloaded + 1
         end do
      end do
   end function cavity_count_unloaded_p_top

   subroutine configure_ocean_wetdry(cfg, ocean_state, grid, compute_rank)
      !! Dynamic wetting/drying (docs/ocean_wetdry_plan.md): copy the
      !! `&ocean_wetdry_nml` knobs onto the BT workstate, allocate the
      !! wd_* workspaces (lazy — absent when the knob is off, so the
      !! default path carries no new arrays and stays byte-identical),
      !! and seed the hysteresis wet mask from the seeded bathymetry
      !! (`barotropic%b` — the same array `configure_ocean_bt_split`
      !! later latches into `bt_H_ref`; `bt_eta` is still 0 here).
      !! Must run BEFORE the restart read (so `wd_wet_dyn` is allocated
      !! + registered when the registry walk runs and a warm restart
      !! overwrites the seed with the saved front state) and BEFORE
      !! `ocean_state_enter_data` (host seeding; the enter_data walk
      !! attaches whatever is allocated).
      type(config_t), intent(in) :: cfg
      type(ocean_state_t), intent(inout) :: ocean_state
      type(hgrid_t), intent(in) :: grid
      integer, intent(in) :: compute_rank
      integer :: nx_w, ny_w, i, j, ng, n_intertidal
      real(wp) :: d0

      ocean_state%dyn%bt_work%wetdry_enable = cfg%ocean%wetdry%enable
      if (.not. cfg%ocean%wetdry%enable) return

      ocean_state%dyn%bt_work%wd_dry_depth = cfg%ocean%wetdry%dry_depth
      ocean_state%dyn%bt_work%wd_rewet_depth = cfg%ocean%wetdry%rewet_depth
      nx_w = grid%nx_total
      ny_w = grid%ny_total
      associate (bt_work => ocean_state%dyn%bt_work)
         allocate (bt_work%wd_wet_dyn(nx_w, ny_w), source=1.0_wp)
         allocate (bt_work%wd_theta(nx_w, ny_w), source=1.0_wp)
         allocate (bt_work%wd_flux_x(nx_w + 1, ny_w), source=0.0_wp)
         allocate (bt_work%wd_flux_y(nx_w, ny_w + 1), source=0.0_wp)
         allocate (bt_work%wd_open_u(nx_w + 1, ny_w), source=1.0_wp)
         allocate (bt_work%wd_open_v(nx_w, ny_w + 1), source=1.0_wp)
         ! Seed the hysteresis mask from the seeded bathymetry (host
         ! loop, pre-enter_data; eta = 0 at configure so D0 = b).  Cells
         ! inside the hysteresis band seed WET — the first substep's own
         ! update settles them.  A restart carrying `wd_wet_dyn`
         ! overwrites this seed (this configure runs BEFORE the registry
         ! read — see the driver ordering comment).
         do j = 1, ny_w
            do i = 1, nx_w
               d0 = ocean_state%barotropic%b(i, j)
               ! `d0 < dry_depth` is the live criterion.  The `wet_mask<=0`
               ! term is belt-and-suspenders: with the knob ON a static-land
               ! column has `b < -land_margin < 0 < dry_depth`, so the first
               ! test already fires — it only guards against a caller seeding
               ! wet_mask with a stricter cutoff than the depth test implies.
               if (d0 < cfg%ocean%wetdry%dry_depth .or. &
                   ocean_state%multilayer%wet_mask(i, j) <= 0.0_wp) then
                  bt_work%wd_wet_dyn(i, j) = 0.0_wp
               end if
            end do
         end do
      end associate
      if (compute_rank == 0) then
         call logger%info("Wet/dry:          ON  dry_depth = "// &
                          to_string(cfg%ocean%wetdry%dry_depth)// &
                          " m, rewet_depth = "// &
                          to_string(cfg%ocean%wetdry%rewet_depth)// &
                          " m (upwind BT faces + positive-definite "// &
                          "outflow limiter + bed-blocking gate)")
         ! v2 semantic trap (plan §10.1): with wet/dry ON the static land
         ! seed cutoff moves to `b < -land_margin`, so any column whose bed
         ! is in the band `[-land_margin, LAND_DEPTH_THRESHOLD)` is NOT
         ! static land any more — it becomes a dynamically-dry INTERTIDAL
         ! flat that floods once eta clears dry_depth.  This affects formula
         ! topos that flag land as b ≈ 0 (island / double_drake), file
         ! bathymetry with a b = 1 m land-flag convention, AND generated
         ! topos like neverworld2 that force continents into the band — so
         ! a name-based check is incomplete.  Scan the seeded interior
         ! bathymetry directly and warn (rank 0) with the exact count when
         ! ANY intertidal-band cell exists.  It's a WARNING, not an abort:
         ! intertidal flats can be intentional; the message just makes the
         ! regime change loud and specific.
         ng = grid%nghost
         n_intertidal = 0
         do j = ng + 1, ny_w - ng
            do i = ng + 1, nx_w - ng
               d0 = ocean_state%barotropic%b(i, j)
               if (d0 >= -cfg%ocean%wetdry%land_margin .and. &
                   d0 < LAND_DEPTH_THRESHOLD) then
                  n_intertidal = n_intertidal + 1
               end if
            end do
         end do
         if (n_intertidal > 0) then
            call logger%warning("Wet/dry: "//to_string(n_intertidal)// &
                                " interior cell(s) have bed elevations in the "// &
                                "intertidal band [-land_margin, "// &
                                "LAND_DEPTH_THRESHOLD) — under the wetdry-aware "// &
                                "static seed (land iff b < -land_margin) these "// &
                                "are LIVE intertidal flats that flood when eta "// &
                                "clears dry_depth, NOT static land.  If you "// &
                                "intended static continents, use bed elevations "// &
                                "b < -land_margin (= "// &
                                to_string(-cfg%ocean%wetdry%land_margin)//" m).")
         end if
      end if
   end subroutine configure_ocean_wetdry

   subroutine configure_ocean_bc(cfg, ocean_state, compute_rank, ierr)
      !! Populate `ocean_state%bc` edge tags + per-edge data values from
      !! `cfg%ocean%bc` (read from `&ocean_bc_nml`).  Run this after
      !! `configure_ocean_bt_split` and before `ocean_state_enter_data`
      !! so the BC state is set before the first dyn step.
      !!
      !! All defaults in `ocean_bc_config_t` resolve to OBC_WALL, so
      !! namelist files that omit `&ocean_bc_nml` are bit-identical to
      !! prior behaviour.
      !!
      !! After populating tags, calls `ocean_bc_validate_periodic` when any
      !! axis is periodic so the ghost-width and sponge incompatibility
      !! checks fire at setup time.
      type(config_t), intent(inout) :: cfg
      type(ocean_state_t), intent(inout) :: ocean_state
      integer, intent(in) :: compute_rank
      integer, intent(out), optional :: ierr
         !! Non-zero on a boundary-condition configuration conflict when
         !! present; absent behaves as today (`error stop`).

      integer :: ntidal, n_tracers
      integer :: i, local_ierr

      associate (bc => ocean_state%bc, bc_cfg => cfg%ocean%bc)

         ! ---- Per-edge tag ----
         bc%west%bc_type = ocean_bc_type_from_string(bc_cfg%west)
         bc%east%bc_type = ocean_bc_type_from_string(bc_cfg%east)
         bc%south%bc_type = ocean_bc_type_from_string(bc_cfg%south)
         bc%north%bc_type = ocean_bc_type_from_string(bc_cfg%north)

         ! ---- Clamped Dirichlet values ----
         bc%west%clamped_eta = bc_cfg%west_clamped_eta
         bc%east%clamped_eta = bc_cfg%east_clamped_eta
         bc%south%clamped_eta = bc_cfg%south_clamped_eta
         bc%north%clamped_eta = bc_cfg%north_clamped_eta
         bc%west%clamped_u = bc_cfg%west_clamped_u
         bc%east%clamped_u = bc_cfg%east_clamped_u
         bc%south%clamped_v = bc_cfg%south_clamped_v
         bc%north%clamped_v = bc_cfg%north_clamped_v

         ! ---- Inflow tracer concentrations ----
         ! Populate clamped_tracer(idx_S) and clamped_tracer(idx_T) for
         ! each edge.  The bc_state_init has already allocated these arrays.
         n_tracers = bc%n_tracers
         if (n_tracers > 0 .and. allocated(bc%west%clamped_tracer)) then
            if (ocean_state%multilayer%idx_salinity > 0 .and. &
                ocean_state%multilayer%idx_salinity <= n_tracers) then
               i = ocean_state%multilayer%idx_salinity
               bc%west%clamped_tracer(i) = bc_cfg%west_inflow_S
               bc%east%clamped_tracer(i) = bc_cfg%east_inflow_S
               bc%south%clamped_tracer(i) = bc_cfg%south_inflow_S
               bc%north%clamped_tracer(i) = bc_cfg%north_inflow_S
            end if
            if (ocean_state%multilayer%idx_temperature > 0 .and. &
                ocean_state%multilayer%idx_temperature <= n_tracers) then
               i = ocean_state%multilayer%idx_temperature
               bc%west%clamped_tracer(i) = bc_cfg%west_inflow_T
               bc%east%clamped_tracer(i) = bc_cfg%east_inflow_T
               bc%south%clamped_tracer(i) = bc_cfg%south_inflow_T
               bc%north%clamped_tracer(i) = bc_cfg%north_inflow_T
            end if
            ! Pseudo-salt OBC mirror: same clamped inflow value as
            ! salinity (pseudo-salt receives exactly salinity's boundary
            ! fluxes, §3.2 of the plan).
            if (ocean_state%multilayer%idx_pseudo_salt > 0 .and. &
                ocean_state%multilayer%idx_pseudo_salt <= n_tracers) then
               i = ocean_state%multilayer%idx_pseudo_salt
               bc%west%clamped_tracer(i) = bc_cfg%west_inflow_S
               bc%east%clamped_tracer(i) = bc_cfg%east_inflow_S
               bc%south%clamped_tracer(i) = bc_cfg%south_inflow_S
               bc%north%clamped_tracer(i) = bc_cfg%north_inflow_S
            end if
         end if

         ! ---- Sponge ----
         bc%west%sponge_width = bc_cfg%sponge_width
         bc%east%sponge_width = bc_cfg%sponge_width
         bc%south%sponge_width = bc_cfg%sponge_width
         bc%north%sponge_width = bc_cfg%sponge_width
         bc%west%sponge_strength = bc_cfg%sponge_strength
         bc%east%sponge_strength = bc_cfg%sponge_strength
         bc%south%sponge_strength = bc_cfg%sponge_strength
         bc%north%sponge_strength = bc_cfg%sponge_strength
         bc%west%sponge_relax_tracers = bc_cfg%sponge_relax_tracers
         bc%east%sponge_relax_tracers = bc_cfg%sponge_relax_tracers
         bc%south%sponge_relax_tracers = bc_cfg%sponge_relax_tracers
         bc%north%sponge_relax_tracers = bc_cfg%sponge_relax_tracers

         ! ---- Tidal constituents ----
         ntidal = min(bc_cfg%west_n_tidal, OBC_MAX_TIDAL_CONSTITUENTS)
         bc%west%n_tidal_constituents = ntidal
         bc%west%tidal_amp(1:ntidal) = bc_cfg%west_tidal_amp(1:ntidal)
         bc%west%tidal_phase(1:ntidal) = bc_cfg%west_tidal_phase(1:ntidal)
         bc%west%tidal_omega(1:ntidal) = bc_cfg%west_tidal_omega(1:ntidal)

         ntidal = min(bc_cfg%east_n_tidal, OBC_MAX_TIDAL_CONSTITUENTS)
         bc%east%n_tidal_constituents = ntidal
         bc%east%tidal_amp(1:ntidal) = bc_cfg%east_tidal_amp(1:ntidal)
         bc%east%tidal_phase(1:ntidal) = bc_cfg%east_tidal_phase(1:ntidal)
         bc%east%tidal_omega(1:ntidal) = bc_cfg%east_tidal_omega(1:ntidal)

         ntidal = min(bc_cfg%south_n_tidal, OBC_MAX_TIDAL_CONSTITUENTS)
         bc%south%n_tidal_constituents = ntidal
         bc%south%tidal_amp(1:ntidal) = bc_cfg%south_tidal_amp(1:ntidal)
         bc%south%tidal_phase(1:ntidal) = bc_cfg%south_tidal_phase(1:ntidal)
         bc%south%tidal_omega(1:ntidal) = bc_cfg%south_tidal_omega(1:ntidal)

         ntidal = min(bc_cfg%north_n_tidal, OBC_MAX_TIDAL_CONSTITUENTS)
         bc%north%n_tidal_constituents = ntidal
         bc%north%tidal_amp(1:ntidal) = bc_cfg%north_tidal_amp(1:ntidal)
         bc%north%tidal_phase(1:ntidal) = bc_cfg%north_tidal_phase(1:ntidal)
         bc%north%tidal_omega(1:ntidal) = bc_cfg%north_tidal_omega(1:ntidal)

         ! ---- OBC tidal nodal/astronomical correction (capability C3) ----
         ! When on, bake the 18.6-yr nodal factor f_c and the equilibrium+nodal
         ! phase (V_c + u_c) into each edge's tidal_fnodal/tidal_arg, using the
         ! SHARED &ocean_tides_nml reference epoch so the boundary tide stays
         ! phase-consistent with the interior body tide (C1).  Host setup —
         ! plain loops, no do concurrent.  Default off ⇒ f=1, arg=0 (the
         ! defaults) ⇒ legacy static-phase OBC sum bit-identical.
         bc%tidal_nodal = bc_cfg%obc_tidal_nodal
         if (bc%tidal_nodal) then
            block
               integer :: yr, mo, dy
               logical :: date_ok
               real(wp) :: dref, dnodal
               real(wp) :: v_all(TIDES_CATALOG_SIZE)
               real(wp) :: f_all(TIDES_CATALOG_SIZE), u_all(TIDES_CATALOG_SIZE)
               character(len=16) :: nodal_str

               ! Equilibrium argument V_c at the model-t=0 reference date.
               call parse_date_string(cfg%ocean%tides%ref_date, yr, mo, dy, date_ok)
               if (.not. date_ok) then
                  call fail("OBC tides: unparseable &ocean_tides_nml ref_date '"// &
                            trim(cfg%ocean%tides%ref_date)//"'", ierr, OCEAN_STATUS_ERR_SETUP)
                  return
               end if
               dref = days_since_1900(yr, mo, dy)

               ! Nodal f/u at the nodal reference date ("" ⇒ ref_date).
               nodal_str = cfg%ocean%tides%nodal_ref_date
               if (len_trim(nodal_str) == 0) nodal_str = cfg%ocean%tides%ref_date
               call parse_date_string(nodal_str, yr, mo, dy, date_ok)
               if (.not. date_ok) then
                  call fail("OBC tides: unparseable &ocean_tides_nml nodal_ref_date '"// &
                            trim(nodal_str)//"'", ierr, OCEAN_STATUS_ERR_SETUP)
                  return
               end if
               dnodal = days_since_1900(yr, mo, dy)

               call equilibrium_arguments(dref, v_all)
               call nodal_fu(dnodal, .true., f_all, u_all)

               if (present(ierr)) then
                  call configure_obc_edge_nodal(bc%west, f_all, u_all, v_all, "west", compute_rank, &
                                                ierr=local_ierr)
                  if (local_ierr /= 0) then
                     ierr = local_ierr
                     return
                  end if
               else
                  call configure_obc_edge_nodal(bc%west, f_all, u_all, v_all, "west", compute_rank)
               end if
               if (present(ierr)) then
                  call configure_obc_edge_nodal(bc%east, f_all, u_all, v_all, "east", compute_rank, &
                                                ierr=local_ierr)
                  if (local_ierr /= 0) then
                     ierr = local_ierr
                     return
                  end if
               else
                  call configure_obc_edge_nodal(bc%east, f_all, u_all, v_all, "east", compute_rank)
               end if
               if (present(ierr)) then
                  call configure_obc_edge_nodal(bc%south, f_all, u_all, v_all, "south", compute_rank, &
                                                ierr=local_ierr)
                  if (local_ierr /= 0) then
                     ierr = local_ierr
                     return
                  end if
               else
                  call configure_obc_edge_nodal(bc%south, f_all, u_all, v_all, "south", compute_rank)
               end if
               if (present(ierr)) then
                  call configure_obc_edge_nodal(bc%north, f_all, u_all, v_all, "north", compute_rank, &
                                                ierr=local_ierr)
                  if (local_ierr /= 0) then
                     ierr = local_ierr
                     return
                  end if
               else
                  call configure_obc_edge_nodal(bc%north, f_all, u_all, v_all, "north", compute_rank)
               end if
            end block
         end if

         ! ---- Open-edge tracer reservoirs (§1, v2) ----
         ! Cache length-scale knobs on bc so kernels can read them without
         ! going back to cfg.  Then allocate reservoir arrays when the feature
         ! is enabled and the edge is open-ish.  The RAW tag is compared on
         ! purpose: an OBC_SPONGE edge still gets a reservoir, which is inert
         ! there (the ghost fill is gated on is_open_ish, which excludes it,
         ! and the update sees the zero outer-face flux, so tres holds its
         ! seed).  Mapping through ocean_bc_outer_face_tag would drop the
         ! bc_tres_* fields from the restart registry.
         bc%res_lscale_out = bc_cfg%res_lscale_out
         bc%res_lscale_in = bc_cfg%res_lscale_in

         if ((bc%res_lscale_out > 0.0_wp .or. bc%res_lscale_in > 0.0_wp) .and. &
             bc%n_tracers > 0) then
            ! West
            if (bc%west%bc_type /= OBC_WALL .and. bc%west%bc_type /= OBC_PERIODIC) then
               allocate (bc%tres_west(bc%ny_total, bc%nz_ml, bc%n_tracers), source=0.0_wp)
            end if
            ! East
            if (bc%east%bc_type /= OBC_WALL .and. bc%east%bc_type /= OBC_PERIODIC) then
               allocate (bc%tres_east(bc%ny_total, bc%nz_ml, bc%n_tracers), source=0.0_wp)
            end if
            ! South
            if (bc%south%bc_type /= OBC_WALL .and. bc%south%bc_type /= OBC_PERIODIC) then
               allocate (bc%tres_south(bc%nx_total, bc%nz_ml, bc%n_tracers), source=0.0_wp)
            end if
            ! North
            if (bc%north%bc_type /= OBC_WALL .and. bc%north%bc_type /= OBC_PERIODIC) then
               allocate (bc%tres_north(bc%nx_total, bc%nz_ml, bc%n_tracers), source=0.0_wp)
            end if

            ! Seed reservoirs from the interior IC concentration.
            ! We seed from clamped_tracer (which was just populated above from
            ! west/east/south/north_inflow_S/T).  This is the best available
            ! "interior" estimate at setup time; the first-step update will
            ! refine it from the actual h_layer state.  Per-layer init is
            ! uniform in k (the IC is typically also uniform in k at setup time).
            if (allocated(bc%tres_west)) then
               do i = 1, bc%n_tracers
                  bc%tres_west(:, :, i) = bc%west%clamped_tracer(i)
               end do
            end if
            if (allocated(bc%tres_east)) then
               do i = 1, bc%n_tracers
                  bc%tres_east(:, :, i) = bc%east%clamped_tracer(i)
               end do
            end if
            if (allocated(bc%tres_south)) then
               do i = 1, bc%n_tracers
                  bc%tres_south(:, :, i) = bc%south%clamped_tracer(i)
               end do
            end if
            if (allocated(bc%tres_north)) then
               do i = 1, bc%n_tracers
                  bc%tres_north(:, :, i) = bc%north%clamped_tracer(i)
               end do
            end if

            if (compute_rank == 0) then
               call logger%info("OBC reservoirs: res_lscale_out=" &
                                //to_string(bc%res_lscale_out)//"m" &
                                //" res_lscale_in="//to_string(bc%res_lscale_in)//"m")
            end if
         end if

         ! ---- Per-layer Orlanski radiation (§2, v2) ----
         ! Parse the radiation_scheme string to an integer tag (0=anomaly, 1=orlanski).
         ! Allocate rx and u_prev arrays on radiating edges when the scheme is orlanski.
         ! Both arrays are initialised to 0; u_prev will be seeded from the IC velocity
         ! on the first ocean_obc_apply_baroclinic call (cold-start: dhdt = 0 ⇒ rx = 0).
         ! Not restart-registered — restarts cold (known gap).
         bc%orlanski_rx_max = bc_cfg%orlanski_rx_max
         bc%orlanski_gamma = bc_cfg%orlanski_gamma
         bc%nudge_tau_in = bc_cfg%nudge_tau_in
         bc%nudge_tau_out = bc_cfg%nudge_tau_out
         select case (trim(bc_cfg%radiation_scheme))
         case ("orlanski")
            bc%radiation_scheme = 1
         case default   ! "anomaly" or anything else: v1 path
            bc%radiation_scheme = 0
         end select

         if (bc%radiation_scheme == 1) then
            ! Raw tags: an OBC_SPONGE edge allocates these buffers but never
            ! uses them (the Orlanski kernels are gated on is_radiating).
            ! West
            if (bc%west%bc_type /= OBC_WALL .and. bc%west%bc_type /= OBC_PERIODIC .and. &
                bc%west%bc_type /= OBC_CLAMPED) then
               allocate (bc%rx_west(bc%ny_total, bc%nz_ml), source=0.0_wp)
               allocate (bc%u_prev_west(bc%ny_total, bc%nz_ml), source=0.0_wp)
            end if
            ! East
            if (bc%east%bc_type /= OBC_WALL .and. bc%east%bc_type /= OBC_PERIODIC .and. &
                bc%east%bc_type /= OBC_CLAMPED) then
               allocate (bc%rx_east(bc%ny_total, bc%nz_ml), source=0.0_wp)
               allocate (bc%u_prev_east(bc%ny_total, bc%nz_ml), source=0.0_wp)
            end if
            ! South
            if (bc%south%bc_type /= OBC_WALL .and. bc%south%bc_type /= OBC_PERIODIC .and. &
                bc%south%bc_type /= OBC_CLAMPED) then
               allocate (bc%rx_south(bc%nx_total, bc%nz_ml), source=0.0_wp)
               allocate (bc%u_prev_south(bc%nx_total, bc%nz_ml), source=0.0_wp)
            end if
            ! North
            if (bc%north%bc_type /= OBC_WALL .and. bc%north%bc_type /= OBC_PERIODIC .and. &
                bc%north%bc_type /= OBC_CLAMPED) then
               allocate (bc%rx_north(bc%nx_total, bc%nz_ml), source=0.0_wp)
               allocate (bc%u_prev_north(bc%nx_total, bc%nz_ml), source=0.0_wp)
            end if

            if (compute_rank == 0) then
               call logger%info("OBC Orlanski: rx_max=" &
                                //to_string(bc%orlanski_rx_max) &
                                //" gamma="//to_string(bc%orlanski_gamma))
            end if
         end if

         ! ---- Full Flather with exterior velocity (§4, v2) ----
         select case (trim(bc_cfg%flather_form))
         case ("full")
            bc%use_full_flather = .true.
         case default   ! "legacy" or anything else
            bc%use_full_flather = .false.
         end select
         bc%ext_u_west = bc_cfg%west_ext_u
         bc%ext_u_east = bc_cfg%east_ext_u
         bc%ext_v_south = bc_cfg%south_ext_v
         bc%ext_v_north = bc_cfg%north_ext_v

         ! ---- Periodic validation (also refreshes periodic_x/y flags) ----
         ! Only need to call if any axis could be periodic.
         if (bc%west%bc_type == OBC_PERIODIC .or. bc%east%bc_type == OBC_PERIODIC .or. &
             bc%south%bc_type == OBC_PERIODIC .or. bc%north%bc_type == OBC_PERIODIC) then
            if (present(ierr)) then
               call ocean_bc_validate_periodic(bc, ierr=local_ierr)
               if (local_ierr /= 0) then
                  ierr = local_ierr
                  return
               end if
            else
               call ocean_bc_validate_periodic(bc)
            end if
         else
            ! Derive periodic flags from final tags even for non-periodic runs.
            bc%periodic_x = .false.
            bc%periodic_y = .false.
         end if

         ! ---- Tripolar north-fold validation (also refreshes north_fold) ----
         ! Always call: it enforces "fold only on north" for every tag set,
         ! and "fold requires periodic w/e + nghost>=3" when the tag is set.
         if (present(ierr)) then
            call ocean_bc_validate_fold(bc, ierr=local_ierr)
            if (local_ierr /= 0) then
               ierr = local_ierr
               return
            end if
         else
            call ocean_bc_validate_fold(bc)
         end if

         if (compute_rank == 0) then
            call logger%info("OBC:  west="//trim(bc_cfg%west)// &
                             " east="//trim(bc_cfg%east)// &
                             " south="//trim(bc_cfg%south)// &
                             " north="//trim(bc_cfg%north))
         end if
      end associate
      if (present(ierr)) ierr = OCEAN_STATUS_OK
   end subroutine configure_ocean_bc

   subroutine configure_ocean_sponge(cfg, ocean_state, grid, compute_rank)
      !! Populate `ocean_state%sponge`'s per-cell `idamp_h`/`idamp_u`/
      !! `idamp_v` maps from `cfg%ocean%sponge` (`&ocean_sponge_nml`) + the
      !! already-configured `ocean_state%bc` edge tags. Run AFTER
      !! `configure_ocean_bc` (reads the edge tags + `has_*` flags) and
      !! BEFORE `ocean_state_enter_data`.
      !!
      !! `damp_source = "band"` (the only value implemented in v1): cosine
      !! ramp from every `OBC_SPONGE`-tagged edge, at the exact cell/u-face/
      !! v-face offsets the legacy `rdb_ocean_sponge::ocean_sponge_apply{,
      !! _tracers}` kernels use (§3.2 of the plan — reproduces today's band
      !! so `enable=.true., damp_source="band"` is *physically* the same
      !! sponge as today). Overlapping edges (corners) SUM their rates — the
      !! exact continuation of the legacy kernel's SEQUENTIAL per-edge decay
      !! (`exp(-a*dt)*exp(-b*dt) = exp(-(a+b)*dt)`).
      !!
      !! Per-edge `west_width`/`west_strength` etc. (sentinel `< 0` ⇒
      !! inherit the single global `&ocean_bc_nml sponge_width`/
      !! `sponge_strength` already fanned out onto `bc%<edge>%sponge_*` by
      !! `configure_ocean_bc`) close the audit gap that the per-edge tag
      !! fields were previously unreachable (no config path could ever make
      !! them differ).
      !!
      !! Does NOT snapshot the reference state — `ocean_sponge_snapshot_reference`
      !! does that, called MUCH earlier from the driver (right after the IC
      !! seed, before any restart read — see that subroutine's docstring for
      !! why the ordering is load-bearing).
      !!
      !! No-op when `.not. ocean_state%sponge%enable` (default) — the maps
      !! stay at their `init`-time zero fill (or unallocated, if `enable`
      !! was false at `init` time too).
      type(config_t), intent(in) :: cfg
      type(ocean_state_t), intent(inout) :: ocean_state
      type(hgrid_t), intent(in) :: grid
      integer, intent(in) :: compute_rank

      integer :: i0, i1, j0, j1, band, ramp
      real(wp) :: strength
      integer :: nonzero_h, nonzero_u, nonzero_v

      associate (sp => ocean_state%sponge, bc => ocean_state%bc, s_cfg => cfg%ocean%sponge)

         if (.not. sp%enable) return

         sp%relax_uv = s_cfg%relax_uv
         sp%relax_tracers = s_cfg%relax_tracers
         sp%relax_h = s_cfg%relax_h
         sp%damp_source = s_cfg%damp_source
         sp%target_source = s_cfg%target_source
         sp%lin_t_ref = s_cfg%lin_t_ref
         sp%lin_dt_dz = s_cfg%lin_dt_dz
         sp%lin_s_ref = s_cfg%lin_s_ref
         sp%lin_ds_dz = s_cfg%lin_ds_dz
         ramp = merge(SPONGE_RAMP_LINEAR, SPONGE_RAMP_COSINE, &
                      trim(s_cfg%ramp) == "linear")

         ! Latch the two registry indices the analytic `linear_z` refresh
         ! needs, so the per-step kernel never walks the tracer registry
         ! (the array-of-derived-types device-indirection rule).
         sp%idx_t = ocean_state%multilayer%idx_temperature
         sp%idx_s = ocean_state%multilayer%idx_salinity

         ! Latch the geopotential depth of the column TOP.  The draft is
         ! static by design, so this is a configure-time copy and the
         ! sponge slot stays self-contained at run time.  No cavity ⇒
         ! `z_draft` is the (1,1) placeholder and `z_top` keeps its
         ! init-time zero (the open-ocean free-surface datum).
         if (ocean_state%metrics%use_cavity .and. &
             size(ocean_state%metrics%z_draft, 1) == size(sp%z_top, 1) .and. &
             size(ocean_state%metrics%z_draft, 2) == size(sp%z_top, 2)) then
            sp%z_top = ocean_state%metrics%z_draft
         else
            sp%z_top = 0.0_wp
         end if

         i0 = grid%nghost + 1
         i1 = grid%nghost + grid%nx_phys
         j0 = grid%nghost + 1
         j1 = grid%nghost + grid%ny_phys

         if (trim(sp%damp_source) == "band") then
            ! ---- West edge ----
            if (bc%west%bc_type == OBC_SPONGE .and. bc%has_west) then
               band = merge(s_cfg%west_width, bc%west%sponge_width, s_cfg%west_width >= 0)
               strength = merge(s_cfg%west_strength, bc%west%sponge_strength, &
                                s_cfg%west_strength >= 0.0_wp)
               if (band > 0) then
                  call sponge_add_band_x(sp%idamp_h, sp%idamp_u, sp%idamp_v, &
                                         wall_face=grid%nghost + 1, band=band, &
                                         strength=strength, side=+1, j0=j0, j1=j1, &
                                         ramp=ramp)
               end if
            end if
            ! ---- East edge ----
            if (bc%east%bc_type == OBC_SPONGE .and. bc%has_east) then
               band = merge(s_cfg%east_width, bc%east%sponge_width, s_cfg%east_width >= 0)
               strength = merge(s_cfg%east_strength, bc%east%sponge_strength, &
                                s_cfg%east_strength >= 0.0_wp)
               if (band > 0) then
                  call sponge_add_band_x(sp%idamp_h, sp%idamp_u, sp%idamp_v, &
                                         wall_face=grid%nghost + grid%nx_phys + 1, band=band, &
                                         strength=strength, side=-1, j0=j0, j1=j1, &
                                         ramp=ramp)
               end if
            end if
            ! ---- South edge ----
            if (bc%south%bc_type == OBC_SPONGE .and. bc%has_south) then
               band = merge(s_cfg%south_width, bc%south%sponge_width, s_cfg%south_width >= 0)
               strength = merge(s_cfg%south_strength, bc%south%sponge_strength, &
                                s_cfg%south_strength >= 0.0_wp)
               if (band > 0) then
                  call sponge_add_band_y(sp%idamp_h, sp%idamp_u, sp%idamp_v, &
                                         wall_face=grid%nghost + 1, band=band, &
                                         strength=strength, side=+1, i0=i0, i1=i1, &
                                         ramp=ramp)
               end if
            end if
            ! ---- North edge ----
            if (bc%north%bc_type == OBC_SPONGE .and. bc%has_north) then
               band = merge(s_cfg%north_width, bc%north%sponge_width, s_cfg%north_width >= 0)
               strength = merge(s_cfg%north_strength, bc%north%sponge_strength, &
                                s_cfg%north_strength >= 0.0_wp)
               if (band > 0) then
                  call sponge_add_band_y(sp%idamp_h, sp%idamp_u, sp%idamp_v, &
                                         wall_face=grid%nghost + grid%ny_phys + 1, band=band, &
                                         strength=strength, side=-1, i0=i0, i1=i1, &
                                         ramp=ramp)
               end if
            end if
         end if

         if (compute_rank == 0) then
            nonzero_h = count(sp%idamp_h > 0.0_wp)
            nonzero_u = count(sp%idamp_u > 0.0_wp)
            nonzero_v = count(sp%idamp_v > 0.0_wp)
            call logger%info("Sponge (map-driven): damp_source="//trim(sp%damp_source)// &
                             " target_source="//trim(sp%target_source)// &
                             " idamp_h max="//to_string(maxval(sp%idamp_h))//" 1/s"// &
                             " nonzero(h/u/v)="//to_string(nonzero_h)//"/"// &
                             to_string(nonzero_u)//"/"//to_string(nonzero_v))
         end if
      end associate
   end subroutine configure_ocean_sponge

   pure subroutine sponge_add_band_x(idamp_h, idamp_u, idamp_v, wall_face, band, &
                                     strength, side, j0, j1, ramp)
      !! Add a cosine-ramp `Idamp` band for a west/east (x-normal) sponge
      !! edge into the three maps, SUMMING onto whatever is already there
      !! (§3.2 corner composition). Offsets reproduce
      !! `rdb_ocean_sponge::ocean_sponge_apply{,_tracers}` exactly:
      !! `idamp_h`/`idamp_v` share the cell-column offset; `idamp_u` (the
      !! x-normal face) sits one further column in for the west edge (`side
      !! = +1`) and shares the offset for the east edge (`side = -1`) — the
      !! legacy kernel's own asymmetry (see Risk 2 of the plan), reproduced
      !! verbatim so `damp_source="band"` matches today's band exactly.
      real(wp), intent(inout) :: idamp_h(:, :), idamp_u(:, :), idamp_v(:, :)
      integer, intent(in) :: wall_face, band, side, j0, j1
      integer, intent(in) :: ramp
         !! `SPONGE_RAMP_COSINE` (default, the legacy shape, bit-identical)
         !! or `SPONGE_RAMP_LINEAR` (ISOMIP+ Eq. 20 at cell centres).
      real(wp), intent(in) :: strength

      integer :: d, j, i_h, i_u
      real(wp) :: rate, alpha

      do d = 0, band - 1
         ! `sponge_band_alpha` holds both shapes; its cosine branch keeps
         ! the legacy kernel's own `acos(-1.0_wp)` spelling of pi
         ! bit-for-bit (rather than `rdb_constants::PI`, a `4*atan(1)`
         ! formulation) — test 9.4 pins the band to 1e-14.
         alpha = sponge_band_alpha(d, band, ramp)
         rate = strength*alpha
         if (side > 0) then
            i_h = wall_face + d
            i_u = wall_face + d + 1
         else
            i_h = wall_face - d - 1
            i_u = wall_face - d - 1
         end if
         if (i_h >= 1 .and. i_h <= size(idamp_h, 1)) then
            do j = j0, j1
               idamp_h(i_h, j) = idamp_h(i_h, j) + rate
            end do
            do j = j0, j1 + 1
               idamp_v(i_h, j) = idamp_v(i_h, j) + rate
            end do
         end if
         if (i_u >= 1 .and. i_u <= size(idamp_u, 1)) then
            do j = j0, j1
               idamp_u(i_u, j) = idamp_u(i_u, j) + rate
            end do
         end if
      end do
   end subroutine sponge_add_band_x

   pure subroutine sponge_add_band_y(idamp_h, idamp_u, idamp_v, wall_face, band, &
                                     strength, side, i0, i1, ramp)
      !! Mirror of `sponge_add_band_x` for a south/north (y-normal) sponge
      !! edge: `idamp_h`/`idamp_u` share the row offset; `idamp_v` (the
      !! y-normal face) sits one further row in for the south edge (`side =
      !! +1`) and shares the offset for the north edge (`side = -1`).
      real(wp), intent(inout) :: idamp_h(:, :), idamp_u(:, :), idamp_v(:, :)
      integer, intent(in) :: wall_face, band, side, i0, i1
      integer, intent(in) :: ramp
         !! See `sponge_add_band_x`.
      real(wp), intent(in) :: strength

      integer :: d, i, j_h, j_v
      real(wp) :: rate, alpha

      do d = 0, band - 1
         alpha = sponge_band_alpha(d, band, ramp)
         rate = strength*alpha
         if (side > 0) then
            j_h = wall_face + d
            j_v = wall_face + d + 1
         else
            j_h = wall_face - d - 1
            j_v = wall_face - d - 1
         end if
         if (j_h >= 1 .and. j_h <= size(idamp_h, 2)) then
            do i = i0, i1
               idamp_h(i, j_h) = idamp_h(i, j_h) + rate
            end do
            do i = i0, i1 + 1
               idamp_u(i, j_h) = idamp_u(i, j_h) + rate
            end do
         end if
         if (j_v >= 1 .and. j_v <= size(idamp_v, 2)) then
            do i = i0, i1
               idamp_v(i, j_v) = idamp_v(i, j_v) + rate
            end do
         end if
      end do
   end subroutine sponge_add_band_y

   subroutine configure_obc_edge_nodal(face, f_all, u_all, v_all, edge_name, compute_rank, ierr)
      !! Bake the C3 nodal/astronomical correction into one OBC edge, then
      !! fail loud (rank-0 error + `error stop`) if any of the edge's
      !! constituent frequencies matches no tide-catalog entry, else log the
      !! resolved constituent → (name, f_c, V_c+u_c) map on rank 0.  Host-side
      !! setup wrapper around the `pure` `obc_tide_nodal_fill` — the pure
      !! helper signals the failure via `fill_ierr`; the loud policy lives
      !! here.
      type(ocean_bc_face_tag_t), intent(inout) :: face
      real(wp), intent(in) :: f_all(TIDES_CATALOG_SIZE)
      real(wp), intent(in) :: u_all(TIDES_CATALOG_SIZE)
      real(wp), intent(in) :: v_all(TIDES_CATALOG_SIZE)
      character(len=*), intent(in) :: edge_name
      integer, intent(in) :: compute_rank
      integer, intent(out), optional :: ierr
         !! Non-zero when an OBC tidal constituent matches no tide-catalog
         !! entry, when present; absent behaves as today (`error stop`).

      integer :: fill_ierr, nc, ic

      call obc_tide_nodal_fill(face, f_all, u_all, v_all, fill_ierr)
      if (fill_ierr /= 0) then
         call fail("OBC tides: "//trim(edge_name)//" edge constituent " &
                   //to_string(fill_ierr)//" omega="//to_string(face%tidal_omega(fill_ierr)) &
                   //" rad/s matches no tide-catalog entry (rel tol 1e-4)", ierr, OCEAN_STATUS_ERR_SETUP)
         return
      end if
      if (compute_rank == 0) then
         do nc = 1, face%n_tidal_constituents
            ic = obc_match_constituent(face%tidal_omega(nc))
            call logger%info("OBC tide nodal ["//trim(edge_name)//"]: "//TIDE_NAME(ic) &
                             //" f="//to_string(face%tidal_fnodal(nc)) &
                             //" V+u="//to_string(face%tidal_arg(nc))//" rad")
         end do
      end if
      if (present(ierr)) ierr = OCEAN_STATUS_OK
   end subroutine configure_obc_edge_nodal

end module rdb_ocean_setup