validate_config Subroutine

public subroutine validate_config(cfg, ierr)

Uses

  • proc~~validate_config~~UsesGraph proc~validate_config validate_config module~rdb_constants rdb_constants proc~validate_config->module~rdb_constants module~rdb_coriolis_adv rdb_coriolis_adv proc~validate_config->module~rdb_coriolis_adv module~rdb_eos rdb_eos proc~validate_config->module~rdb_eos module~rdb_ocean_bottom_drag rdb_ocean_bottom_drag proc~validate_config->module~rdb_ocean_bottom_drag module~rdb_ocean_boundary_types rdb_ocean_boundary_types proc~validate_config->module~rdb_ocean_boundary_types module~rdb_ocean_cavity rdb_ocean_cavity proc~validate_config->module~rdb_ocean_cavity module~rdb_ocean_cavity_melt rdb_ocean_cavity_melt proc~validate_config->module~rdb_ocean_cavity_melt module~rdb_ocean_horizontal_viscosity rdb_ocean_horizontal_viscosity proc~validate_config->module~rdb_ocean_horizontal_viscosity module~rdb_ocean_lateral_mix rdb_ocean_lateral_mix proc~validate_config->module~rdb_ocean_lateral_mix module~rdb_ocean_pressure_force rdb_ocean_pressure_force proc~validate_config->module~rdb_ocean_pressure_force module~rdb_ocean_pseudo_salt rdb_ocean_pseudo_salt proc~validate_config->module~rdb_ocean_pseudo_salt module~rdb_ocean_surface_flux rdb_ocean_surface_flux proc~validate_config->module~rdb_ocean_surface_flux module~rdb_ocean_tidal_mixing rdb_ocean_tidal_mixing proc~validate_config->module~rdb_ocean_tidal_mixing module~rdb_ocean_top_drag rdb_ocean_top_drag proc~validate_config->module~rdb_ocean_top_drag module~rdb_ocean_vmix rdb_ocean_vmix proc~validate_config->module~rdb_ocean_vmix module~rdb_recon_weno rdb_recon_weno proc~validate_config->module~rdb_recon_weno module~rdb_vcoord rdb_vcoord proc~validate_config->module~rdb_vcoord pic_types pic_types module~rdb_constants->pic_types module~rdb_coriolis_adv->module~rdb_constants iso_fortran_env iso_fortran_env module~rdb_coriolis_adv->iso_fortran_env module~rdb_barotropic_state rdb_barotropic_state module~rdb_coriolis_adv->module~rdb_barotropic_state module~rdb_grid rdb_grid module~rdb_coriolis_adv->module~rdb_grid module~rdb_mem_report rdb_mem_report module~rdb_coriolis_adv->module~rdb_mem_report module~rdb_multilayer_state rdb_multilayer_state module~rdb_coriolis_adv->module~rdb_multilayer_state module~rdb_ocean_metrics rdb_ocean_metrics module~rdb_coriolis_adv->module~rdb_ocean_metrics module~rdb_ocean_porous rdb_ocean_porous module~rdb_coriolis_adv->module~rdb_ocean_porous module~rdb_scratch_3d rdb_scratch_3d module~rdb_coriolis_adv->module~rdb_scratch_3d module~rdb_eos->module~rdb_constants module~rdb_eos->module~rdb_grid module~rdb_ocean_bottom_drag->module~rdb_constants module~rdb_ocean_bottom_drag->iso_fortran_env module~rdb_ocean_bottom_drag->module~rdb_grid module~rdb_ocean_bottom_drag->module~rdb_mem_report module~rdb_ocean_bottom_drag->module~rdb_multilayer_state module~rdb_ocean_bottom_drag->module~rdb_ocean_metrics module~rdb_ocean_bottom_drag->module~rdb_scratch_3d module~rdb_ocean_boundary_types->module~rdb_constants module~rdb_ocean_boundary_types->iso_fortran_env module~rdb_ocean_boundary_types->module~rdb_grid module~rdb_ocean_boundary_types->module~rdb_mem_report module~rdb_ocean_status rdb_ocean_status module~rdb_ocean_boundary_types->module~rdb_ocean_status module~rdb_ocean_tide_astro rdb_ocean_tide_astro module~rdb_ocean_boundary_types->module~rdb_ocean_tide_astro pic_ascii pic_ascii module~rdb_ocean_boundary_types->pic_ascii pic_logger pic_logger module~rdb_ocean_boundary_types->pic_logger module~rdb_ocean_cavity->module~rdb_constants module~rdb_ocean_cavity->module~rdb_grid module~rdb_ocean_cavity_melt->module~rdb_constants module~rdb_ocean_cavity_melt->module~rdb_eos ieee_arithmetic ieee_arithmetic module~rdb_ocean_cavity_melt->ieee_arithmetic module~rdb_ocean_horizontal_viscosity->module~rdb_constants module~rdb_ocean_horizontal_viscosity->module~rdb_ocean_lateral_mix module~rdb_ocean_horizontal_viscosity->iso_fortran_env module~rdb_ocean_horizontal_viscosity->module~rdb_grid module~rdb_ocean_horizontal_viscosity->module~rdb_mem_report module~rdb_ocean_horizontal_viscosity->module~rdb_multilayer_state module~rdb_ocean_horizontal_viscosity->module~rdb_ocean_metrics module~rdb_ocean_horizontal_viscosity->module~rdb_scratch_3d module~rdb_ocean_lateral_mix->module~rdb_constants module~rdb_ocean_lateral_mix->iso_fortran_env module~rdb_ocean_lateral_mix->module~rdb_grid module~rdb_ocean_lateral_mix->module~rdb_mem_report module~rdb_ocean_lateral_mix->module~rdb_multilayer_state module~rdb_ocean_lateral_mix->module~rdb_ocean_metrics module~rdb_ocean_lateral_mix->module~rdb_scratch_3d module~rdb_ocean_pressure_force->module~rdb_constants module~rdb_ocean_pressure_force->module~rdb_eos module~rdb_ocean_pressure_force->iso_fortran_env module~rdb_ocean_pressure_force->module~rdb_grid module~rdb_ocean_pressure_force->module~rdb_mem_report module~rdb_ocean_pressure_force->module~rdb_multilayer_state module~rdb_ocean_pressure_force->module~rdb_ocean_metrics module~rdb_ocean_pgf_reconstruct rdb_ocean_pgf_reconstruct module~rdb_ocean_pressure_force->module~rdb_ocean_pgf_reconstruct module~rdb_ocean_pressure_force->module~rdb_scratch_3d module~rdb_ocean_pseudo_salt->module~rdb_constants module~rdb_error_ring rdb_error_ring module~rdb_ocean_pseudo_salt->module~rdb_error_ring module~rdb_ocean_pseudo_salt->module~rdb_grid module~rdb_ocean_pseudo_salt->module~rdb_multilayer_state module~rdb_ocean_pseudo_salt->pic_logger module~rdb_ocean_surface_flux->module~rdb_constants module~rdb_ocean_surface_flux->iso_fortran_env module~rdb_ocean_surface_flux->module~rdb_grid module~rdb_ocean_surface_flux->module~rdb_mem_report module~rdb_ocean_surface_flux->module~rdb_multilayer_state module~rdb_ocean_tidal_mixing->module~rdb_constants module~rdb_ocean_tidal_mixing->module~rdb_eos module~rdb_ocean_tidal_mixing->iso_fortran_env module~rdb_ocean_tidal_mixing->module~rdb_grid module~rdb_ocean_tidal_mixing->module~rdb_mem_report module~rdb_ocean_tidal_mixing->module~rdb_multilayer_state module~rdb_ocean_top_drag->module~rdb_constants module~rdb_ocean_top_drag->iso_fortran_env module~rdb_ocean_top_drag->module~rdb_grid module~rdb_ocean_top_drag->module~rdb_mem_report module~rdb_ocean_top_drag->module~rdb_multilayer_state module~rdb_ocean_top_drag->module~rdb_scratch_3d module~rdb_ocean_vmix->module~rdb_constants module~rdb_ocean_vmix->module~rdb_eos module~rdb_ocean_vmix->module~rdb_ocean_surface_flux module~rdb_ocean_vmix->iso_fortran_env module~rdb_ocean_vmix->module~rdb_grid module~rdb_ocean_vmix->module~rdb_mem_report module~rdb_ocean_vmix->module~rdb_multilayer_state module~rdb_ocean_surface_stress rdb_ocean_surface_stress module~rdb_ocean_vmix->module~rdb_ocean_surface_stress module~rdb_recon_weno->module~rdb_constants module~rdb_recon_weno->module~rdb_grid module~rdb_vcoord->module~rdb_constants module~rdb_vcoord->pic_logger pic_strings pic_strings module~rdb_vcoord->pic_strings module~rdb_barotropic_state->module~rdb_constants module~rdb_barotropic_state->iso_fortran_env module~rdb_barotropic_state->module~rdb_grid module~rdb_barotropic_state->module~rdb_mem_report module~rdb_error_ring->pic_logger module~rdb_grid->module~rdb_constants module~rdb_mem_report->module~rdb_constants module~rdb_mem_report->iso_fortran_env module~rdb_mem_report->pic_logger module~rdb_mem_report->pic_strings module~rdb_multilayer_state->module~rdb_constants module~rdb_multilayer_state->iso_fortran_env module~rdb_multilayer_state->module~rdb_error_ring module~rdb_multilayer_state->module~rdb_grid module~rdb_multilayer_state->module~rdb_mem_report module~rdb_multilayer_state->pic_logger module~rdb_efp rdb_efp module~rdb_multilayer_state->module~rdb_efp module~rdb_tracer rdb_tracer module~rdb_multilayer_state->module~rdb_tracer module~rdb_ocean_metrics->module~rdb_constants module~rdb_ocean_metrics->iso_fortran_env module~rdb_ocean_metrics->module~rdb_error_ring module~rdb_ocean_metrics->module~rdb_grid module~rdb_ocean_metrics->module~rdb_mem_report module~rdb_ocean_metrics->module~rdb_ocean_status module~rdb_ocean_metrics->pic_logger module~rdb_ocean_metrics->pic_strings module~rdb_io_netcdf rdb_io_netcdf module~rdb_ocean_metrics->module~rdb_io_netcdf module~rdb_ocean_bipolar rdb_ocean_bipolar module~rdb_ocean_metrics->module~rdb_ocean_bipolar module~rdb_ocean_fold rdb_ocean_fold module~rdb_ocean_metrics->module~rdb_ocean_fold netcdf netcdf module~rdb_ocean_metrics->netcdf module~rdb_ocean_pgf_reconstruct->module~rdb_constants module~rdb_ocean_pgf_reconstruct->module~rdb_eos module~rdb_ocean_porous->module~rdb_constants module~rdb_ocean_porous->ieee_arithmetic module~rdb_ocean_surface_stress->module~rdb_constants module~rdb_ocean_surface_stress->iso_fortran_env module~rdb_ocean_surface_stress->module~rdb_grid module~rdb_ocean_surface_stress->module~rdb_mem_report module~rdb_ocean_surface_stress->module~rdb_multilayer_state module~rdb_ocean_surface_stress->module~rdb_scratch_3d module~rdb_ocean_tide_astro->module~rdb_constants module~rdb_ocean_tide_astro->iso_fortran_env module~rdb_scratch_3d->module~rdb_constants module~rdb_scratch_3d->iso_fortran_env module~rdb_scratch_3d->module~rdb_mem_report module~rdb_efp->ieee_arithmetic module~rdb_efp->iso_fortran_env module~rdb_io_netcdf->module~rdb_constants module~rdb_io_netcdf->iso_fortran_env module~rdb_io_netcdf->module~rdb_error_ring module~rdb_io_netcdf->pic_logger module~rdb_io_netcdf->pic_strings module~rdb_io_netcdf->netcdf iso_c_binding iso_c_binding module~rdb_io_netcdf->iso_c_binding module~rdb_ocean_bipolar->module~rdb_constants module~rdb_ocean_fold->module~rdb_constants module~rdb_tracer->module~rdb_constants module~rdb_tracer->iso_fortran_env module~rdb_tracer->module~rdb_grid module~rdb_tracer->module~rdb_mem_report

Validate configuration parameters after reading

Logs errors for invalid values that would crash the solver, and warnings for suspicious but non-fatal settings.

ierr, when present, returns OCEAN_STATUS_ERR_CONFIG_VALIDATE instead of error stop-ing on the first accumulated failure (every individual check still logs via global_logger%error unchanged); absent behaves as today.

Arguments

Type IntentOptional Attributes Name
type(config_t), intent(in) :: cfg
integer, intent(out), optional :: ierr

Non-zero on any cross-knob semantic validation failure when present; absent behaves as today (error stop).


Calls

proc~~validate_config~~CallsGraph proc~validate_config validate_config cav_dz cav_dz proc~validate_config->cav_dz error error proc~validate_config->error info info proc~validate_config->info proc~aniso_mode_is_implemented aniso_mode_is_implemented proc~validate_config->proc~aniso_mode_is_implemented proc~bbl_glue_is_effective bbl_glue_is_effective proc~validate_config->proc~bbl_glue_is_effective proc~bdrag_variant_is_implemented bdrag_variant_is_implemented proc~validate_config->proc~bdrag_variant_is_implemented proc~bkgnd_henyey_conflicts_profile bkgnd_henyey_conflicts_profile proc~validate_config->proc~bkgnd_henyey_conflicts_profile proc~cavity_draft_is_uniform cavity_draft_is_uniform proc~validate_config->proc~cavity_draft_is_uniform proc~check_dataovr_file check_dataovr_file proc~validate_config->proc~check_dataovr_file proc~dataovr_any_tag_set dataovr_any_tag_set proc~validate_config->proc~dataovr_any_tag_set proc~dataovr_entry_is_valid dataovr_entry_is_valid proc~validate_config->proc~dataovr_entry_is_valid proc~dataovr_freshwater_needs_components dataovr_freshwater_needs_components proc~validate_config->proc~dataovr_freshwater_needs_components proc~dataovr_time_is_valid dataovr_time_is_valid proc~validate_config->proc~dataovr_time_is_valid proc~diag_density_levels_ok diag_density_levels_ok proc~validate_config->proc~diag_density_levels_ok proc~error_ring_push error_ring_push proc~validate_config->proc~error_ring_push proc~fail fail proc~validate_config->proc~fail proc~forcing_components_need_thermo forcing_components_need_thermo proc~validate_config->proc~forcing_components_need_thermo proc~gprime_nz_is_supported gprime_nz_is_supported proc~validate_config->proc~gprime_nz_is_supported proc~has_biharmonic_backstop has_biharmonic_backstop proc~validate_config->proc~has_biharmonic_backstop proc~ice_hlim_count ice_hlim_count proc~validate_config->proc~ice_hlim_count proc~ice_hlim_spec_is_valid ice_hlim_spec_is_valid proc~validate_config->proc~ice_hlim_spec_is_valid proc~ice_ic_parse_conc_config ice_ic_parse_conc_config proc~validate_config->proc~ice_ic_parse_conc_config proc~ice_t_freeze ice_t_freeze proc~validate_config->proc~ice_t_freeze proc~kpp_sw_method_is_implemented kpp_sw_method_is_implemented proc~validate_config->proc~kpp_sw_method_is_implemented proc~lateral_closure_conflicts_smag_ah lateral_closure_conflicts_smag_ah proc~validate_config->proc~lateral_closure_conflicts_smag_ah proc~lateral_closure_is_implemented lateral_closure_is_implemented proc~validate_config->proc~lateral_closure_is_implemented proc~leith_biharm_is_inert leith_biharm_is_inert proc~validate_config->proc~leith_biharm_is_inert proc~nz_stack_is_sufficient nz_stack_is_sufficient proc~validate_config->proc~nz_stack_is_sufficient proc~nz_stack_required nz_stack_required proc~validate_config->proc~nz_stack_required proc~ocean_bc_type_from_string ocean_bc_type_from_string proc~validate_config->proc~ocean_bc_type_from_string proc~ocean_bt_rem_from_visc_rem_on ocean_bt_rem_from_visc_rem_on proc~validate_config->proc~ocean_bt_rem_from_visc_rem_on proc~ocean_bt_visc_rem_producer_on ocean_bt_visc_rem_producer_on proc~validate_config->proc~ocean_bt_visc_rem_producer_on proc~p_top_has_producer p_top_has_producer proc~validate_config->proc~p_top_has_producer proc~parse_bdrag_variant parse_bdrag_variant proc~validate_config->proc~parse_bdrag_variant proc~parse_cavity_draft_sign parse_cavity_draft_sign proc~validate_config->proc~parse_cavity_draft_sign proc~parse_cavity_exchange_law parse_cavity_exchange_law proc~validate_config->proc~parse_cavity_exchange_law proc~parse_cavity_freshwater parse_cavity_freshwater proc~validate_config->proc~parse_cavity_freshwater proc~parse_cavity_ice_mode parse_cavity_ice_mode proc~validate_config->proc~parse_cavity_ice_mode proc~parse_cavity_volume_comp parse_cavity_volume_comp proc~validate_config->proc~parse_cavity_volume_comp proc~parse_lateral_closure parse_lateral_closure proc~validate_config->proc~parse_lateral_closure proc~parse_opgf_variant parse_opgf_variant proc~validate_config->proc~parse_opgf_variant proc~parse_pv_adv_scheme parse_pv_adv_scheme proc~validate_config->proc~parse_pv_adv_scheme proc~parse_pv_variant parse_pv_variant proc~validate_config->proc~parse_pv_variant proc~parse_tdrag_variant parse_tdrag_variant proc~validate_config->proc~parse_tdrag_variant proc~parse_tfreeze_set parse_tfreeze_set proc~validate_config->proc~parse_tfreeze_set proc~parse_tracer_recon parse_tracer_recon proc~validate_config->proc~parse_tracer_recon proc~parse_vcoord_type parse_vcoord_type proc~validate_config->proc~parse_vcoord_type proc~parse_z_fixed_profile parse_z_fixed_profile proc~validate_config->proc~parse_z_fixed_profile proc~pseudo_salt_conflicts_ice pseudo_salt_conflicts_ice proc~validate_config->proc~pseudo_salt_conflicts_ice proc~pseudo_salt_conflicts_restore pseudo_salt_conflicts_restore proc~validate_config->proc~pseudo_salt_conflicts_restore proc~pseudo_salt_needs_thermo_warning pseudo_salt_needs_thermo_warning proc~validate_config->proc~pseudo_salt_needs_thermo_warning proc~pv_adv_required_nghost pv_adv_required_nghost proc~validate_config->proc~pv_adv_required_nghost proc~pv_adv_scheme_is_implemented pv_adv_scheme_is_implemented proc~validate_config->proc~pv_adv_scheme_is_implemented proc~pv_variant_is_implemented pv_variant_is_implemented proc~validate_config->proc~pv_variant_is_implemented proc~sponge_ramp_is_valid sponge_ramp_is_valid proc~validate_config->proc~sponge_ramp_is_valid proc~sponge_source_is_implemented sponge_source_is_implemented proc~validate_config->proc~sponge_source_is_implemented proc~sponge_target_is_implemented sponge_target_is_implemented proc~validate_config->proc~sponge_target_is_implemented proc~substep_drag_ignores_bdrag_form substep_drag_ignores_bdrag_form proc~validate_config->proc~substep_drag_ignores_bdrag_form proc~sw_source_is_implemented sw_source_is_implemented proc~validate_config->proc~sw_source_is_implemented proc~tdrag_variant_is_implemented tdrag_variant_is_implemented proc~validate_config->proc~tdrag_variant_is_implemented proc~tidal_mixing_is_inert tidal_mixing_is_inert proc~validate_config->proc~tidal_mixing_is_inert proc~tracer_recon_required_nghost tracer_recon_required_nghost proc~validate_config->proc~tracer_recon_required_nghost proc~vcoord_h_min_is_coherent vcoord_h_min_is_coherent proc~validate_config->proc~vcoord_h_min_is_coherent proc~warn_unknown_bc warn_unknown_bc proc~validate_config->proc~warn_unknown_bc proc~z_fixed_nominal_dz z_fixed_nominal_dz proc~validate_config->proc~z_fixed_nominal_dz proc~zfixed_cavity_nu_h_below_envelope zfixed_cavity_nu_h_below_envelope proc~validate_config->proc~zfixed_cavity_nu_h_below_envelope to_string to_string proc~validate_config->to_string warning warning proc~validate_config->warning zf_dz zf_dz proc~validate_config->zf_dz proc~check_dataovr_file->error proc~fail->error proc~fail->proc~error_ring_push proc~ice_hlim_spec_is_valid->proc~ice_hlim_count proc~ice_hlim_spec_is_valid->to_string proc~parse_kpp_sw_method parse_kpp_sw_method proc~kpp_sw_method_is_implemented->proc~parse_kpp_sw_method proc~nz_stack_is_sufficient->proc~nz_stack_required to_lower to_lower proc~ocean_bc_type_from_string->to_lower proc~ocean_bt_visc_rem_producer_on->proc~ocean_bt_rem_from_visc_rem_on proc~ocean_bt_forcing_visc_rem_on ocean_bt_forcing_visc_rem_on proc~ocean_bt_visc_rem_producer_on->proc~ocean_bt_forcing_visc_rem_on proc~ocean_bt_renorm_visc_rem_on ocean_bt_renorm_visc_rem_on proc~ocean_bt_visc_rem_producer_on->proc~ocean_bt_renorm_visc_rem_on proc~vcoord_h_min_role vcoord_h_min_role proc~vcoord_h_min_is_coherent->proc~vcoord_h_min_role proc~warn_unknown_bc->warning proc~zfixed_cavity_nu_h_below_envelope->proc~parse_vcoord_type

Called by

proc~~validate_config~~CalledByGraph proc~validate_config validate_config proc~build_pending_handle build_pending_handle proc~build_pending_handle->proc~validate_config proc~rdb_ocean_create_from_string rdb_ocean_create_from_string proc~rdb_ocean_create_from_string->proc~build_pending_handle proc~rdb_ocean_create_pending rdb_ocean_create_pending proc~rdb_ocean_create_pending->proc~build_pending_handle

Variables

Type Visibility Attributes Name Initial
logical, private :: has_error
integer, private :: lateral_closure_code

Source Code

   subroutine validate_config(cfg, ierr)
      !! Validate configuration parameters after reading
      !!
      !! Logs errors for invalid values that would crash the solver,
      !! and warnings for suspicious but non-fatal settings.
      !!
      !! `ierr`, when present, returns `OCEAN_STATUS_ERR_CONFIG_VALIDATE`
      !! instead of `error stop`-ing on the first accumulated failure
      !! (every individual check still logs via `global_logger%error`
      !! unchanged); absent behaves as today.
      use rdb_ocean_lateral_mix, only: parse_lateral_closure, &
                                       lateral_closure_is_implemented, &
                                       lateral_closure_conflicts_smag_ah, &
                                       has_biharmonic_backstop, &
                                       leith_biharm_is_inert
      use rdb_ocean_horizontal_viscosity, only: aniso_mode_is_implemented
      use rdb_vcoord, only: parse_vcoord_type, vcoord_h_min_is_coherent, &
                            parse_z_fixed_profile, z_fixed_nominal_dz, &
                            ZFIXED_PROFILE_INVALID, ZFIXED_PROFILE_UNIFORM, &
                            ZFIXED_PROFILE_LIST, ZFIXED_DZ_OK, ZFIXED_DZ_ERR_COUNT, &
                            ZFIXED_DZ_ERR_TOO_DEEP
      use rdb_constants, only: VCOORD_SIGMA, VCOORD_ZSTAR, VCOORD_EULERIAN_Z, &
                               VCOORD_ZSIGMA, VCOORD_LAGRANGIAN, VCOORD_ZSTAR_SIGMA, &
                               VCOORD_ZSTAR_FULL, VCOORD_Z_FIXED, VCOORD_RHO, VCOORD_HYCOM, &
                               H_VANISHED
      use rdb_ocean_boundary_types, only: ocean_bc_type_from_string, OBC_PERIODIC, OBC_WALL, &
                                          OBC_INVALID
      use rdb_coriolis_adv, only: parse_pv_variant, pv_variant_is_implemented, &
                                  parse_pv_adv_scheme, pv_adv_scheme_is_implemented, &
                                  pv_adv_required_nghost
      use rdb_ocean_bottom_drag, only: parse_bdrag_variant, bdrag_variant_is_implemented
      use rdb_ocean_pressure_force, only: parse_opgf_variant, gprime_nz_is_supported, &
                                          OPGF_VARIANT_FV_MOM6
      use rdb_ocean_tidal_mixing, only: tidal_mixing_is_inert
      use rdb_ocean_pseudo_salt, only: pseudo_salt_conflicts_restore, &
                                       pseudo_salt_conflicts_ice, &
                                       pseudo_salt_needs_thermo_warning
      use rdb_ocean_surface_flux, only: sw_source_is_implemented
      use rdb_ocean_vmix, only: kpp_sw_method_is_implemented, &
                                bkgnd_henyey_conflicts_profile
      use rdb_eos, only: parse_tfreeze_set, TFREEZE_SET_INVALID
      use rdb_ocean_top_drag, only: parse_tdrag_variant, tdrag_variant_is_implemented, &
                                    TDRAG_LINEAR, TDRAG_QUADRATIC
      use rdb_ocean_cavity_melt, only: parse_cavity_exchange_law, parse_cavity_ice_mode, &
                                       CAVITY_LAW_INVALID, CAVITY_LAW_CONST_GAMMA, &
                                       CAVITY_LAW_HJ99, CAVITY_LAW_YUNG25, &
                                       CAVITY_ICE_INVALID, CAVITY_ICE_INSULATING, &
                                       CAVITY_ICE_ADV_DIFF, &
                                       parse_cavity_freshwater, parse_cavity_volume_comp, &
                                       CAVITY_FW_INVALID, CAVITY_FW_VIRTUAL, CAVITY_FW_MASS, &
                                       CAVITY_VC_INVALID, CAVITY_VC_NONE, CAVITY_VC_UNIFORM_OPEN
      use rdb_ocean_cavity, only: parse_cavity_draft_sign, CAVITY_SIGN_INVALID
      type(config_t), intent(in) :: cfg
      integer, intent(out), optional :: ierr
         !! Non-zero on any cross-knob semantic validation failure when
         !! present; absent behaves as today (`error stop`).

      logical :: has_error
      integer :: lateral_closure_code

      has_error = .false.

      ! Simulation regime
      if (trim(cfg%sim_type) /= "ocean") then
         call logger%error("Invalid sim_type = '"//trim(cfg%sim_type)// &
                           "': must be 'ocean' (the coastal regime was split out into "// &
                           "its own repository)")
         has_error = .true.
      end if

      ! Grid parameters
      if (cfg%nx < 1) then
         call logger%error("Invalid nx = "//to_string(cfg%nx)//": must be >= 1")
         has_error = .true.
      end if
      if (cfg%ny < 1) then
         call logger%error("Invalid ny = "//to_string(cfg%ny)//": must be >= 1")
         has_error = .true.
      end if
      if (cfg%nghost < 1) then
         call logger%error("Invalid nghost = "//to_string(cfg%nghost)//": must be >= 1")
         has_error = .true.
      end if
      if (cfg%dx <= 0.0_wp) then
         call logger%error("Invalid dx = "//to_string(cfg%dx)//": must be > 0")
         has_error = .true.
      end if
      if (cfg%dy <= 0.0_wp) then
         call logger%error("Invalid dy = "//to_string(cfg%dy)//": must be > 0")
         has_error = .true.
      end if

      ! Time parameters
      if (cfg%cfl <= 0.0_wp .or. cfg%cfl > 1.0_wp) then
         call logger%error("Invalid cfl = "//to_string(cfg%cfl)//": must be in (0, 1]")
         has_error = .true.
      end if
      if (cfg%t_end <= 0.0_wp) then
         call logger%error("Invalid t_end = "//to_string(cfg%t_end)//": must be > 0")
         has_error = .true.
      end if

      ! Multilayer parameters
      if (cfg%use_multilayer) then
         if (cfg%nz_layers < 1) then
            call logger%error("Invalid nz_layers = "//to_string(cfg%nz_layers)// &
                              ": must be >= 1 for multilayer")
            has_error = .true.
         end if
      end if

      ! Per-column stack-workspace bound.  FAIL LOUD, both regimes.
      !
      ! Every layered kernel (BPG, ALE remap, kappa-shear, Redi, the
      ! diag-remap fills) carries fixed-size `NZ_STACK_MAX` thread-local
      ! column arrays.  Overrunning them corrupts thread-local storage
      ! SILENTLY — wrong answers, no crash — so this must refuse the run,
      ! not warn.  It replaces a warning-only check that additionally
      ! never fired on the ocean path: no ocean namelist sets
      ! `use_multilayer` (it defaults .false.), so the ocean regime had no
      ! nz guard at all beyond the wavespeed- and Redi-specific ones.
      !
      ! The requirement is `nz + 1` — see `nz_stack_required`.
      if (cfg%use_multilayer .or. trim(cfg%sim_type) == "ocean") then
         if (.not. nz_stack_is_sufficient(cfg%nz_layers)) then
            call logger%error("nz_layers = "//to_string(cfg%nz_layers)// &
                              " needs NZ_STACK_MAX >= "// &
                              to_string(nz_stack_required(cfg%nz_layers))// &
                              " but this binary was compiled with NZ_STACK_MAX = "// &
                              to_string(NZ_STACK_MAX)// &
                              ". Per-column stack kernels (BPG, ALE remap, kappa-shear, "// &
                              "Redi, diag remap) would overrun thread-local storage and "// &
                              "silently produce wrong answers. Rebuild with "// &
                              "-DRDB_NZ_STACK_MAX="// &
                              to_string(nz_stack_required(cfg%nz_layers))//" (or larger).")
            has_error = .true.
         end if
      end if

      ! Ocean windowed-drain tracer_recon guard (Q6).  A SEPARATE knob from
      ! the coastal tracer_recon above — it lives in `&ocean_vmix_nml` and
      ! only takes effect on sim_type='ocean' (ignored on coastal, like all
      ! ocean knobs).  Reuses the coastal parse + per-rung nghost helpers
      ! but NOT the coastal support-status predicate (which rejects ocean by
      ! design).  Fail loud on pqm/unknown and on an insufficient nghost for
      ! the requested rung (weno5→3, weno7→4, weno9→5).  The default nghost
      ! is 3 (bumped from 2 — see the grid config default), so ppm/weno5 are
      ! satisfied by default and only weno7/9 need an explicit extra column.
      if (trim(cfg%sim_type) == "ocean") then
         block
            use rdb_recon_weno, only: parse_tracer_recon, &
                                      tracer_recon_required_nghost, &
                                      TRACER_RECON_PPM
            integer :: ocn_recon_code
            ocn_recon_code = parse_tracer_recon(trim(cfg%ocean%vmix%tracer_recon))
            if (ocn_recon_code == -2) then
               call logger%error("&ocean_vmix_nml tracer_recon='pqm' is not implemented; "// &
                                 "use ppm, weno5, weno7 or weno9")
               has_error = .true.
            else if (ocn_recon_code < 0) then
               call logger%error("&ocean_vmix_nml tracer_recon='"// &
                                 trim(cfg%ocean%vmix%tracer_recon)// &
                                 "' not recognised; valid: ppm, weno5, weno7, weno9")
               has_error = .true.
            else if (ocn_recon_code /= TRACER_RECON_PPM) then
               if (cfg%nghost < tracer_recon_required_nghost(ocn_recon_code)) then
                  call logger%error("&ocean_vmix_nml tracer_recon='"// &
                                    trim(cfg%ocean%vmix%tracer_recon)// &
                                    "' requires nghost >= "// &
                                    to_string(tracer_recon_required_nghost(ocn_recon_code))// &
                                    " but nghost = "//to_string(cfg%nghost))
                  has_error = .true.
               end if
            end if
         end block
      end if

      ! (PR-21) Shortwave source + boundary-layer coupling selectors.
      ! Fail-loud on unknown strings (PR-6 idiom); q_sw source requires
      ! the PR-12 component set (makes the host-side source branch total);
      ! the net-heat source with penetrating SW warns about the
      ! night-time negative-I0 hazard (§2.4 of the plan).
      if (trim(cfg%sim_type) == "ocean") then
         if (.not. sw_source_is_implemented(trim(cfg%ocean%thermo%sw_source))) then
            call logger%error("&ocean_thermo_nml sw_source='"// &
                              trim(cfg%ocean%thermo%sw_source)// &
                              "' not recognised; valid: net_heat, q_sw")
            has_error = .true.
         end if
         if (.not. kpp_sw_method_is_implemented(trim(cfg%ocean%thermo%kpp_sw_method))) then
            call logger%error("&ocean_thermo_nml kpp_sw_method='"// &
                              trim(cfg%ocean%thermo%kpp_sw_method)// &
                              "' not recognised; valid: all_sw, mxl_sw, lv1_sw")
            has_error = .true.
         end if
         if (trim(cfg%ocean%thermo%sw_source) == "q_sw" .and. &
             .not. cfg%ocean%forcing%enable_components) then
            call logger%error("&ocean_thermo_nml sw_source='q_sw' requires "// &
                              "&ocean_forcing_nml enable_components=.true. "// &
                              "(q_sw is allocated only with the component set)")
            has_error = .true.
         end if
         if (trim(cfg%ocean%thermo%sw_source) == "net_heat" .and. &
             cfg%ocean%thermo%sw_pen_frac > 0.0_wp) then
            call logger%warning("shortwave penetration is reading the NET heat flux "// &
                                "(&ocean_thermo_nml sw_source='net_heat', sw_pen_frac>0): "// &
                                "I0 < 0 wherever Q_heat < 0, which drives unphysical "// &
                                "negative irradiance down the two-band profile. Set "// &
                                "sw_source='q_sw' with &ocean_forcing_nml "// &
                                "enable_components=.true.")
         end if
      end if

      ! Sponge width
      if (cfg%sponge_width < 0) then
         call logger%error("Invalid sponge_width = "//to_string(cfg%sponge_width)// &
                           ": must be >= 0")
         has_error = .true.
      end if

      ! Warn about unknown BC strings (they silently default to WALL)
      call warn_unknown_bc(cfg%bc_west, "bc_west")
      call warn_unknown_bc(cfg%bc_east, "bc_east")
      call warn_unknown_bc(cfg%bc_south, "bc_south")
      call warn_unknown_bc(cfg%bc_north, "bc_north")

      ! `thickness_config` is consumed only by the ocean state builder's IC
      ! seed.  The coastal regimes lay their layers on a separate path and
      ! would silently ignore a non-default value, so fail loud instead.
      if (trim(cfg%thickness_config) /= "sigma" .and. &
          trim(cfg%sim_type) /= "ocean") then
         call logger%error("thickness_config = '"//trim(cfg%thickness_config)// &
                           "' is an ocean-path knob (sim_type = 'ocean'); the coastal "// &
                           "regimes seed their layers on a separate path and would "// &
                           "silently ignore it")
         has_error = .true.
      end if

      ! Retired coastal-legacy `&tracer_nml` linear-EOS quartet.  These
      ! four keys reach `tracer_t%eos_coeff`/`eos_ref` and nothing else;
      ! the C-grid ocean path's linear EOS reads `&ocean_ic_nml` alone.
      ! A knob that validates and silently does nothing is the bug, so
      ! moving any of them off its historical default is fatal and names
      ! the live replacement rather than being quietly ignored.
      if (cfg%alpha_T /= LEGACY_TRACER_ALPHA_T) then
         call logger%error("&tracer_nml alpha_T is RETIRED on the ocean path: it only ever "// &
                           "reached tracer_t%eos_coeff, which no ocean kernel reads, so "// &
                           "setting it changed nothing. Use &ocean_ic_nml alpha_T "// &
                           "(kg/m^3 per degC) instead, or delete the key.")
         has_error = .true.
      end if
      if (cfg%beta_S /= LEGACY_TRACER_BETA_S) then
         call logger%error("&tracer_nml beta_S is RETIRED on the ocean path: it only ever "// &
                           "reached tracer_t%eos_coeff, which no ocean kernel reads, so "// &
                           "setting it changed nothing. Use &ocean_ic_nml beta_S "// &
                           "(kg/m^3 per PSU) instead, or delete the key.")
         has_error = .true.
      end if
      if (cfg%T_ref /= LEGACY_TRACER_T_REF) then
         call logger%error("&tracer_nml T_ref is RETIRED on the ocean path: it only ever "// &
                           "reached tracer_t%eos_ref, which no ocean kernel reads, so "// &
                           "setting it changed nothing. Use &ocean_ic_nml T_ref (degC) "// &
                           "instead, or delete the key.")
         has_error = .true.
      end if
      if (cfg%S_ref /= LEGACY_TRACER_S_REF) then
         call logger%error("&tracer_nml S_ref is RETIRED on the ocean path: it only ever "// &
                           "reached tracer_t%eos_ref, which no ocean kernel reads, so "// &
                           "setting it changed nothing. Use &ocean_ic_nml S_ref (PSU) "// &
                           "instead, or delete the key.")
         has_error = .true.
      end if

      ! Linear-EOS stratified salinity IC: both ends must be set (the
      ! seed gates on `/= 0` for BOTH, mirroring T_init_surface/bottom),
      ! so exactly one non-zero is a silently-uniform column — the same
      ! class of bug the retired knobs above were.
      if ((cfg%S_init_surface /= 0.0_wp) .neqv. (cfg%S_init_bottom /= 0.0_wp)) then
         call logger%error("&tracer_nml S_init_surface / S_init_bottom must BOTH be "// &
                           "non-zero to build the linear S(z) IC (they gate together, "// &
                           "exactly as T_init_surface/T_init_bottom do); one alone is "// &
                           "ignored and the column stays uniform at initial_salinity.")
         has_error = .true.
      end if

      ! Freezing-point (liquidus) coefficient set.  Belt-and-braces on top
      ! of the `nml_enum allowed=` list, in the style of the
      ! `conc_config` check above — and NOT optional: `parse_tfreeze_set`
      ! deliberately has no default fallback, so a string that reaches
      ! `eos_apply_tfreeze_set` unrecognised would silently leave the
      ! sea-ice set in place.  At S = 34.5 the two shipped sets differ by
      ! ~0.03 degC, which is enough to flip the sign of an ice-shelf
      ! basal melt rate — a mistyped liquidus must stop the run.
      if (parse_tfreeze_set(cfg%ocean%eos%tfreeze_set) == TFREEZE_SET_INVALID) then
         call logger%error("&ocean_eos_nml tfreeze_set = '"// &
                           trim(cfg%ocean%eos%tfreeze_set)// &
                           "' is not recognised (allowed: seaice, isomip). "// &
                           "'seaice' is the SIS2/MOM6 sea-ice liquidus "// &
                           "(-0.054*S - 7.53e-8*p); 'isomip' is the ISOMIP+ "// &
                           "ice-shelf-cavity set (-0.0573*S + 0.0832 - 7.53e-8*p, "// &
                           "Asay-Davis et al. 2016 Table 4).")
         has_error = .true.
      end if

      ! Ocean diag manager
      if (trim(cfg%sim_type) == "ocean") then
         if (trim(cfg%ocean%diag%vgrid) /= "layer" .and. &
             trim(cfg%ocean%diag%vgrid) /= "z_fixed" .and. &
             trim(cfg%ocean%diag%vgrid) /= "sigma" .and. &
             trim(cfg%ocean%diag%vgrid) /= "zstar" .and. &
             trim(cfg%ocean%diag%vgrid) /= "density") then
            call logger%error("Invalid ocean_diag_vgrid = '"// &
                              trim(cfg%ocean%diag%vgrid)// &
                              "': must be 'layer', 'z_fixed', 'sigma', 'zstar', or 'density'")
            has_error = .true.
         end if
         if (trim(cfg%ocean%diag%vgrid) == "z_fixed" .and. &
             cfg%ocean%diag%n_z_levels <= 0) then
            call logger%error("ocean_diag_vgrid = 'z_fixed' requires "// &
                              "ocean_diag_n_z_levels > 0")
            has_error = .true.
         end if
         if (cfg%ocean%diag%n_z_levels < 0 .or. &
             cfg%ocean%diag%n_z_levels > MAX_OCEAN_DIAG_Z_LEVELS) then
            call logger%error("ocean_diag_n_z_levels = "// &
                              to_string(cfg%ocean%diag%n_z_levels)// &
                              " out of range [0, "// &
                              to_string(MAX_OCEAN_DIAG_Z_LEVELS)//"]")
            has_error = .true.
         end if
         ! Density bins have no auto-fill (unlike sigma/z*), so the global
         ! vgrid='density' selection OR any per-diagnostic ':density'/':rho'
         ! attribute in `diags` requires a valid rho_levels list.
         if (.not. diag_density_levels_ok(trim(cfg%ocean%diag%vgrid), &
                                          trim(cfg%ocean%diag%diags), &
                                          cfg%ocean%diag%n_rho_levels, &
                                          cfg%ocean%diag%rho_levels)) then
            call logger%error("&ocean_diag_nml vgrid='density' (or a ':density'/':rho' "// &
                              "entry in diags) requires n_rho_levels > 0, "// &
                              "rho_levels(1:n_rho_levels) strictly increasing, and "// &
                              "n_rho_levels <= "//to_string(MAX_OCEAN_DIAG_Z_LEVELS))
            has_error = .true.
         end if
         if (cfg%ocean%diag%enabled .and. cfg%ocean%diag%dt_out <= 0.0_wp) then
            call logger%error("ocean_diag_dt_out must be > 0 when "// &
                              "ocean_diag_enabled = .true.")
            has_error = .true.
         end if
         if (cfg%ocean%diag%enabled .and. cfg%ocean%diag%dt_out > cfg%t_end) then
            call logger%warning("ocean_diag_dt_out ("// &
                                to_string(cfg%ocean%diag%dt_out)//" s) > t_end ("// &
                                to_string(cfg%t_end)//" s): the diagnostic will "// &
                                "fire at most once — is dt_out in your time_unit "// &
                                "(e.g. days), not seconds?")
         end if
         if (trim(cfg%ocean%topo%topo_config) /= "flat" .and. &
             trim(cfg%ocean%topo%topo_config) /= "spoon" .and. &
             trim(cfg%ocean%topo%topo_config) /= "seamount" .and. &
             trim(cfg%ocean%topo%topo_config) /= "neverworld2" .and. &
             trim(cfg%ocean%topo%topo_config) /= "island" .and. &
             trim(cfg%ocean%topo%topo_config) /= "double_drake" .and. &
             trim(cfg%ocean%topo%topo_config) /= "isomip_plus" .and. &
             trim(cfg%ocean%topo%topo_config) /= "file") then
            call logger%error("Invalid topo_config = '"//trim(cfg%ocean%topo%topo_config)// &
                              "': must be 'flat', 'spoon', 'seamount', 'neverworld2', "// &
                              "'island', 'double_drake', 'isomip_plus', or 'file'")
            has_error = .true.
         end if
         if (trim(cfg%ocean%ic%ic_config) /= "" .and. &
             trim(cfg%ocean%ic%ic_config) /= "eady" .and. &
             trim(cfg%ocean%ic%ic_config) /= "geostrophic_adjustment" .and. &
             trim(cfg%ocean%ic%ic_config) /= "baroclinic_jet") then
            call logger%error("Invalid ic_config = '"//trim(cfg%ocean%ic%ic_config)// &
                              "': must be '', 'eady', 'geostrophic_adjustment', or "// &
                              "'baroclinic_jet'")
            has_error = .true.
         end if
         if (trim(cfg%ocean%topo%topo_config) == "file" .and. &
             len_trim(cfg%bathymetry_file) == 0) then
            call logger%error("topo_config = 'file' requires bathymetry_file "// &
                              "to be set in &output_nml")
            has_error = .true.
         end if
         if (trim(cfg%ocean%topo%wind_config) /= "constant" .and. &
             trim(cfg%ocean%topo%wind_config) /= "2gyre" .and. &
             trim(cfg%ocean%topo%wind_config) /= "neverworld2") then
            call logger%error("Invalid wind_config = '"//trim(cfg%ocean%topo%wind_config)// &
                              "': must be 'constant', '2gyre', or 'neverworld2'")
            has_error = .true.
         end if
         if (cfg%ocean%topo%max_depth <= 0.0_wp) then
            call logger%error("ocean_max_depth must be > 0")
            has_error = .true.
         end if
         if (trim(cfg%ocean%topo%topo_config) == "spoon") then
            if (cfg%ocean%topo%edge_depth <= 0.0_wp) then
               call logger%error("ocean_edge_depth must be > 0 for spoon bathymetry")
               has_error = .true.
            end if
            if (cfg%ocean%topo%edge_depth >= cfg%ocean%topo%max_depth) then
               call logger%error("ocean_edge_depth must be < ocean_max_depth")
               has_error = .true.
            end if
            if (cfg%ocean%topo%slope_scale <= 0.0_wp) then
               call logger%error("ocean_slope_scale must be > 0 for spoon bathymetry")
               has_error = .true.
            end if
         end if

         ! `uniform_z` seeds collapsed layers at the isopycnal `angstrom_h`
         ! floor, whereas the wet/dry path pins the emerged-column invariant
         ! `Sum h_layer = nz*2*H_VANISHED` and floors `bt_h` to exactly that
         ! same value.  Mixing the two would seed a bt_h that disagrees with
         ! Sum h_layer on every emerged column, so refuse the combination.
         if (trim(cfg%thickness_config) == "uniform_z" .and. &
             cfg%ocean%wetdry%enable) then
            call logger%error("thickness_config = 'uniform_z' is incompatible with "// &
                              "&ocean_wetdry_nml enable = .true.: the emerged-column "// &
                              "seed invariant (bt_h = nz*2*H_VANISHED) assumes the "// &
                              "'sigma' even split")
            has_error = .true.
         end if

         ! Stretched z* nominal profile.  Default "uniform" ⇒ no check
         ! fires and nothing downstream changes.  Anything else must be on
         ! a family that reads it — `z_fixed` (its levels), `zstar` (MOM6 z*
         ! levels) or `hycom` (its z* nominal floor); silently ignoring it
         ! would be the bug — and must build: list length = nz_layers, tanh
         ! parameters in range and leaving room to stretch.
         block
            integer :: zf_code, zf_ierr
            real(wp), allocatable :: zf_dz(:)
            zf_code = parse_z_fixed_profile(cfg%z_fixed_profile)
            if (zf_code == ZFIXED_PROFILE_INVALID) then
               call logger%error("&vcoord_nml z_fixed_profile = '"// &
                                 trim(cfg%z_fixed_profile)//"' is not one of "// &
                                 "'uniform', 'list', 'tanh'")
               has_error = .true.
            else if (zf_code /= ZFIXED_PROFILE_UNIFORM) then
               if (parse_vcoord_type(cfg%vcoord_type, VCOORD_EULERIAN_Z) /= VCOORD_Z_FIXED .and. &
                   parse_vcoord_type(cfg%vcoord_type, VCOORD_EULERIAN_Z) /= VCOORD_ZSTAR .and. &
                   parse_vcoord_type(cfg%vcoord_type, VCOORD_EULERIAN_Z) /= VCOORD_HYCOM) then
                  call logger%error("&vcoord_nml z_fixed_profile = '"// &
                                    trim(cfg%z_fixed_profile)//"' is only read by "// &
                                    "vcoord_type = 'z_fixed', 'zstar' or 'hycom' (got '"// &
                                    trim(cfg%vcoord_type)//"'); it would be silently ignored")
                  has_error = .true.
               else if (cfg%nz_layers >= 1) then
                  allocate (zf_dz(cfg%nz_layers))
                  call z_fixed_nominal_dz(zf_code, cfg%nz_layers, cfg%ocean%topo%max_depth, &
                                          cfg%z_fixed_dz, cfg%z_fixed_dz_top, &
                                          cfg%z_fixed_tanh_center, cfg%z_fixed_tanh_width, &
                                          zf_dz, zf_ierr)
                  if (zf_ierr == ZFIXED_DZ_ERR_COUNT) then
                     call logger%error("&vcoord_nml z_fixed_profile = 'list' needs exactly "// &
                                       "nz_layers = "//to_string(cfg%nz_layers)// &
                                       " leading positive z_fixed_dz entries (surface first, "// &
                                       "no gaps), got "//to_string(count(cfg%z_fixed_dz > 0.0_wp)))
                     has_error = .true.
                  else if (zf_ierr == ZFIXED_DZ_ERR_TOO_DEEP) then
                     call logger%error("&vcoord_nml z_fixed_profile = 'tanh': nz_layers * "// &
                                       "z_fixed_dz_top = "// &
                                       to_string(real(cfg%nz_layers, wp)*cfg%z_fixed_dz_top)// &
                                       " m is not below &ocean_topo_nml max_depth = "// &
                                       to_string(cfg%ocean%topo%max_depth)// &
                                       " m — there is no depth left to stretch into")
                     has_error = .true.
                  else if (zf_ierr /= ZFIXED_DZ_OK) then
                     call logger%error("&vcoord_nml z_fixed_profile = 'tanh' needs "// &
                                       "&ocean_topo_nml max_depth > 0, z_fixed_dz_top > 0, "// &
                                       "z_fixed_tanh_width > 0 and 0 <= z_fixed_tanh_center <= 1")
                     has_error = .true.
                  else if (zf_code == ZFIXED_PROFILE_LIST .and. &
                           sum(zf_dz) < cfg%ocean%topo%max_depth) then
                     call logger%warning("&vcoord_nml z_fixed_dz sums to "// &
                                         to_string(sum(zf_dz))//" m, shallower than "// &
                                         "&ocean_topo_nml max_depth = "// &
                                         to_string(cfg%ocean%topo%max_depth)// &
                                         " m: columns deeper than the profile carry the "// &
                                         "excess in their bed layer")
                  end if
               end if
            end if
         end block

         ! Target-density profile of the density families.  Default
         ! "uniform" ⇒ the light→dense linspace, nothing checked.  "list"
         ! must be on rho/hycom (nothing else reads it) and give exactly
         ! nz_layers+1 strictly increasing interface densities, light first.
         block
            integer :: n_rho, kr, vc_code
            logical :: rho_ok
            vc_code = parse_vcoord_type(cfg%vcoord_type, VCOORD_EULERIAN_Z)
            if (trim(cfg%rho_target_profile) /= "uniform" .and. &
                trim(cfg%rho_target_profile) /= "list") then
               call logger%error("&vcoord_nml rho_target_profile = '"// &
                                 trim(cfg%rho_target_profile)//"' is not one of "// &
                                 "'uniform', 'list'")
               has_error = .true.
            else if (trim(cfg%rho_target_profile) == "list") then
               if (vc_code /= VCOORD_RHO .and. vc_code /= VCOORD_HYCOM) then
                  call logger%error("&vcoord_nml rho_target_profile = 'list' is only "// &
                                    "read by vcoord_type = 'rho' or 'hycom' (got '"// &
                                    trim(cfg%vcoord_type)//"'); it would be silently ignored")
                  has_error = .true.
               else
                  ! Leading positive entries, no gaps.
                  n_rho = 0
                  do kr = 1, size(cfg%rho_target_list)
                     if (cfg%rho_target_list(kr) <= 0.0_wp) exit
                     n_rho = n_rho + 1
                  end do
                  rho_ok = n_rho == cfg%nz_layers + 1 .and. &
                           count(cfg%rho_target_list > 0.0_wp) == n_rho
                  if (rho_ok) then
                     do kr = 2, n_rho
                        if (cfg%rho_target_list(kr) <= cfg%rho_target_list(kr - 1)) then
                           rho_ok = .false.
                        end if
                     end do
                  end if
                  if (.not. rho_ok) then
                     call logger%error("&vcoord_nml rho_target_profile = 'list' needs "// &
                                       "exactly nz_layers+1 = "// &
                                       to_string(cfg%nz_layers + 1)// &
                                       " leading positive, strictly increasing "// &
                                       "rho_target_list entries (lightest first, no gaps), "// &
                                       "got "//to_string(n_rho))
                     has_error = .true.
                  end if
               end if
            else if (count(cfg%rho_target_list > 0.0_wp) > 0) then
               call logger%error("&vcoord_nml rho_target_list is set but "// &
                                 "rho_target_profile = 'uniform' ignores it; set "// &
                                 "rho_target_profile = 'list'")
               has_error = .true.
            end if
         end block

         ! `VCOORD_ZSIGMA` is NOT a working coordinate on the ocean path.
         ! Its deep branch reads `z_ref_global` as a table of absolute
         ! reference depths in METRES
         ! (`z_top_k = min(z_ref_global(nz-k), column_total)`,
         ! `rdb_ocean_vcoord :: ocean_vcoord_compute_target_h_impl`), but the
         ! ONLY writer of that array anywhere in `src/` is the DIMENSIONLESS
         ! `z_ref_global(k) = k/nz` init in `ocean_vcoord_init` — nothing on
         ! the namelist path or the Python path ever replaces it with metres.
         ! So every z-level interval is `1/nz` metres, every layer collapses,
         ! and the whole column is dumped into `target_h(:,:,1)` (the BED
         ! layer) by the deficit line.  `Sum target_h = H + eta` still holds,
         ! which is exactly why no conservation test ever caught it; the
         ! placement is measured interface-by-interface in
         ! `test_ocean_vcoord_interface_depths ::
         ! documents_zsigma_dimensionless_zref_collapse` (nine 0.1 m layers
         ! in the top 90 cm of a 1000 m column).
         !
         ! Refuse it rather than silently running a broken coordinate.  No
         ! shipped namelist selects it.  Follow-up: fill `z_ref_global` in
         ! metres (the family is the natural seat for a sigma-near-the-top /
         ! z-below HYBRID), then delete this refusal and the `documents_*`
         ! test with it.  `VCOORD_ZSTAR_SIGMA` consumes the same table
         ! FRACTIONALLY (rescaled by `z_ref_global(nz)`) so it is unaffected
         ! by the units — but note that with a uniform table its deep branch
         ! is numerically indistinguishable from SIGMA.
         if (parse_vcoord_type(cfg%vcoord_type, default_code=VCOORD_EULERIAN_Z) &
             == VCOORD_ZSIGMA) then
            call logger%error("&vcoord_nml vcoord_type = 'zsigma' is refused on the "// &
                              "ocean path: its deep branch reads `z_ref_global` as "// &
                              "absolute depths in METRES, but the only writer of that "// &
                              "table is the dimensionless `k/nz` init, so every "// &
                              "z-level interval is 1/nz metres and the whole column "// &
                              "collapses into the bed layer (the column sum is still "// &
                              "exact, which is why it looked healthy).  Use 'sigma', "// &
                              "'zstar', 'zstar_sigma' or 'zstar_full'; ZSIGMA returns "// &
                              "when `z_ref_global` is filled in metres.")
            has_error = .true.
         end if

         ! `&vcoord_nml zstar_h_min` carries TWO different contracts, picked
         ! by the coordinate family rather than by the value (see
         ! `rdb_vcoord :: vcoord_h_min_role`).  On the GEOMETRIC families
         ! (ZSTAR_FULL / Z_FIXED) it is the anti-zero thickness of below-bed
         ! FILLER layers that are meant to read as vanished downstream, so it
         ! belongs at or below the D4 skip/merge marker `H_VANISHED`; on the
         ! DENSITY families (RHO / HYCOM) the collapsed layers carry real
         ! tracer mass and that path deliberately floors at
         ! `max(zstar_h_min, 2*H_VANISHED)` instead.  Nothing else pins the
         ! knob, so an INERT-role run with `zstar_h_min > H_VANISHED` silently
         ! promotes its below-bed filler to LIVE layers (real EOS density off
         ! ghost T/S, a PGF column entry, a remap-drain concentration, a vdiff
         ! interface) while the coordinate still treats them as throwaway.
         !
         ! This was a WARNING until the rigid-top work made it blocking.  The
         ! reason it is now an ERROR: a coordinate that vanishes layers
         ! against the TOP of the column (an ice-shelf cavity) puts its
         ! fillers where the surface fluxes, the pressure gradient, the melt
         ! sampler and the tracer budgets all read — so a filler that is not
         ! skipped is not a cosmetic slip, it is a conservation hole.  The
         ! only configuration in the tree that sat in the band was the repo's
         ! own Python worked example (`python/tests/test_worked_example.py`,
         ! `ZStarFull(h_min=1.0e-3)`), corrected in the same change.
         !
         ! NOTE the boundary this must NOT move: five shipped namelists set
         ! `zstar_h_min = 1.5e-4`, H_VANISHED EXACTLY, which is legal —
         ! `vcoord_h_min_is_coherent` is a strict `>` and every downstream
         ! vanish test is a strict `> H_VANISHED`, so a layer on the marker
         ! reads as vanished.  Relaxing either to `>=` would refuse the
         ! canonical double-gyre reference; `test_ocean_vcoord_hygiene ::
         ! h_min_on_the_marker_is_accepted` guards that.
         !
         ! A non-positive floor was already refused and still is — it defeats
         ! the knob's single documented purpose.
         block
            integer :: hmin_vcoord_code
            hmin_vcoord_code = parse_vcoord_type(cfg%vcoord_type, &
                                                 default_code=VCOORD_EULERIAN_Z)
            if (.not. vcoord_h_min_is_coherent(hmin_vcoord_code, cfg%zstar_h_min)) then
               if (cfg%zstar_h_min <= 0.0_wp) then
                  call logger%error("&vcoord_nml zstar_h_min = "// &
                                    to_string(cfg%zstar_h_min)//" must be > 0: it exists "// &
                                    "so a vanishing layer's target thickness is never "// &
                                    "exactly zero (kernels that divide by h_layer)")
               else
                  call logger%error("&vcoord_nml zstar_h_min = "// &
                                    to_string(cfg%zstar_h_min)//" m exceeds H_VANISHED = "// &
                                    to_string(H_VANISHED)//" m under vcoord_type = '"// &
                                    trim(cfg%vcoord_type)//"': that family uses the knob "// &
                                    "as an anti-zero floor for filler layers that are "// &
                                    "MEANT to stay vanished — above H_VANISHED they "// &
                                    "become dynamically live (EOS/PGF/remap-drain/vdiff) "// &
                                    "while the coordinate still treats them as "// &
                                    "throwaway.  Use a value <= "// &
                                    to_string(H_VANISHED)//"; for a genuinely live "// &
                                    "minimum layer thickness use &ocean_isopycnal_nml "// &
                                    "angstrom_h (the D4 floor knob); the rho/hycom "// &
                                    "regrid has its own keep-alive floor")
               end if
               has_error = .true.
            end if
            ! NOTE the boundary, deliberately NOT warned about at runtime:
            ! the five shipped namelists set `zstar_h_min = 1.5e-4`, which is
            ! H_VANISHED EXACTLY.  That is legal — every downstream vanish
            ! test is a strict `> H_VANISHED`, so a layer sitting on the
            ! marker still reads as vanished — but it carries zero margin,
            ! and any gate relaxed to `>=` would change those runs' answers.
            ! A per-run warning here would fire on the canonical double-gyre
            ! reference forever while recommending nothing an operator can do
            ! without changing answers, so the fact lives in the docs and at
            ! the gate (`rdb_ocean_remap :: H_FLOOR`) instead.
         end block

         ! GM thickness diffusion consumes the stored isopycnal slope, so
         ! the slopes slot MUST be enabled.  Loud invariant (the kernel is
         ! pure/device and cannot fail loud); a silent no-op would hide a
         ! misconfigured GM run.
         if (cfg%ocean%gm%enable .and. .not. cfg%ocean%slopes%enable) then
            call logger%error("&ocean_gm_nml enable=.true. requires "// &
                              "&ocean_slopes_nml enable=.true. (GM reads the stored slope)")
            has_error = .true.
         end if
         ! khth_slope_max enters as 1/slope_max^2 in the safe-streamfunction
         ! limiter; a zero/negative value divides by zero.
         if (cfg%ocean%gm%enable .and. cfg%ocean%gm%khth_slope_max <= 0.0_wp) then
            call logger%error("&ocean_gm_nml khth_slope_max must be > 0 "// &
                              "(enters the safe-streamfunction limiter as 1/slope_max^2)")
            has_error = .true.
         end if

         ! Redi (capability [3]) ships the CONTINUOUS variant only; the
         ! discontinuous (regula-falsi) path is deferred (R4).  Loud
         ! invariant — the device kernel cannot fail loud.
         if (cfg%ocean%redi%enable .and. .not. cfg%ocean%redi%continuous) then
            call logger%error("&ocean_redi_nml continuous=.false. (discontinuous "// &
                              "variant) is not implemented yet (deferred R4)")
            has_error = .true.
         end if

         ! VarMix (capability [4]) needs the stored slope + N² (slopes slot)
         ! for the Eady term AND the first-mode cg1 (wavespeed slot) for the
         ! resolution function.  Loud invariant — the kernel is pure/device.
         if (cfg%ocean%varmix%enable .and. .not. cfg%ocean%slopes%enable) then
            call logger%error("&ocean_varmix_nml enable=.true. requires "// &
                              "&ocean_slopes_nml enable=.true. (VarMix reads the stored slope + N^2)")
            has_error = .true.
         end if
         if (cfg%ocean%varmix%enable .and. .not. cfg%ocean%wavespeed%enable) then
            call logger%error("&ocean_varmix_nml enable=.true. requires "// &
                              "&ocean_wavespeed_nml enable=.true. (the resolution function needs cg1)")
            has_error = .true.
         end if

         ! Resolution-scaled momentum viscosity (Gap 1) reads the VarMix
         ! resolution function `Res_fn`, so VarMix MUST be enabled.  Loud
         ! invariant — the lateral-mix kernel is pure/device.
         if (cfg%ocean%hvisc%resoln_scaled_visc .and. .not. cfg%ocean%varmix%enable) then
            call logger%error("&ocean_hvisc_nml resoln_scaled_visc=.true. requires "// &
                              "&ocean_varmix_nml enable=.true. (the resolution function lives in VarMix)")
            has_error = .true.
         end if

         ! MEKE (capability [5]) sources its eddy energy from the GM PE
         ! release (`gm%gm_src`), so GM MUST be enabled.  Loud invariant —
         ! the kernel is pure/device and cannot fail loud.  (The VarMix
         ! feedback seam is OPTIONAL: with VarMix off, MEKE still evolves E
         ! but `meke%kh` has no face accumulator to feed — documented, not an
         ! error.)
         if (cfg%ocean%meke%enable .and. .not. cfg%ocean%gm%enable) then
            call logger%error("&ocean_meke_nml enable=.true. requires "// &
                              "&ocean_gm_nml enable=.true. (MEKE sources from gm_src)")
            has_error = .true.
         end if

         ! ---- Lateral-mixing closure: fail loud on unimplemented tags ----
         ! `parse_lateral_closure` maps the &ocean_hvisc_nml string to an
         ! LMIX_* code; a mistyped/garbage string yields LMIX_INVALID and a
         ! tag with no dispatcher path is not implemented.  Either case must
         ! ABORT here — never silently fall through to background-only
         ! viscosity (the original silent-wrong-answer foot-gun).
         lateral_closure_code = parse_lateral_closure(cfg%ocean%hvisc%lateral_closure)
         if (.not. lateral_closure_is_implemented(lateral_closure_code)) then
            call logger%error("Invalid/unimplemented &ocean_hvisc_nml lateral_closure = '"// &
                              trim(cfg%ocean%hvisc%lateral_closure)//"': must be one of "// &
                              "'none', 'leith', 'smagorinsky'/'smag', 'biharmonic', "// &
                              "'leith_biharm' — a closure with no kernel must not "// &
                              "silently disable lateral viscosity")
            has_error = .true.
         end if
         ! `leith_biharm` and `smag_ah` are BOTH flow-aware biharmonic
         ! closures that fill the same `nu4_face_*` arrays; the dispatcher
         ! runs the closure first, then `compute_smag_ah` unconditionally,
         ! so an enabled `smag_ah` would silently OVERWRITE the
         ! Leith-biharmonic fill.  Fail loud rather than let one closure
         ! silently win (MOM6 max-combines them; we do not).
         if (lateral_closure_conflicts_smag_ah(lateral_closure_code, &
                                               cfg%ocean%hvisc%smag_ah)) then
            call logger%error("&ocean_hvisc_nml lateral_closure='leith_biharm' and "// &
                              "smag_ah=.true. are both flow-aware biharmonic closures "// &
                              "filling nu4_face — smag_ah would overwrite the "// &
                              "Leith-biharmonic fill; enable only one")
            has_error = .true.
         end if
         ! Only anisotropy mode 0 (grid-relative `aniso_dir`) has an
         ! implemented direction tensor; a requested-but-unimplemented mode
         ! must abort, not silently fall back to the grid-i default.
         if (.not. aniso_mode_is_implemented(cfg%ocean%hvisc%aniso_mode)) then
            call logger%error("&ocean_hvisc_nml aniso_mode must be 0 "// &
                              "(grid-relative aniso_dir) — other modes are not "// &
                              "implemented and must not silently fall back to grid-i")
            has_error = .true.
         end if
         ! MEKE backscatter injects a NEGATIVE harmonic viscosity that
         ! amplifies grid-scale modes; only a POSITIVE biharmonic
         ! (nu_4 / smag_ah / leith_biharm) can dissipate them.  Abort if
         ! backscatter is on without a biharmonic backstop configured —
         ! the CFL floor bounds the growth rate, it does not make a
         ! negative harmonic operator stable on its own.
         if (cfg%ocean%meke%backscatter .and. &
             .not. has_biharmonic_backstop(cfg%ocean%hvisc%nu_4, &
                                           cfg%ocean%hvisc%smag_ah, &
                                           cfg%ocean%hvisc%smag_bi_const, &
                                           lateral_closure_code, &
                                           cfg%ocean%hvisc%c_leith_bi, &
                                           cfg%ocean%hvisc%nu_4_bg)) then
            call logger%error("&ocean_meke_nml backscatter=.true. requires a "// &
                              "biharmonic backstop with a NON-ZERO coefficient "// &
                              "(&ocean_hvisc_nml nu_4>0 with no flow-aware "// &
                              "closure selected, or smag_ah=.true. with "// &
                              "smag_bi_const>0, or lateral_closure='leith_biharm' "// &
                              "with c_leith_bi>0 — or nu_4_bg>0 as a floor in "// &
                              "either flow-aware case) — the negative harmonic "// &
                              "backscatter is unstable without one")
            has_error = .true.
         end if

         ! ---- PR-6 fail-loud dispatch pack ----
         ! Each of these string→enum / config→kernel seams previously
         ! accepted a value the schema (or a hand-written reader) advertised,
         ! then silently ran DIFFERENT physics than the name promised.  Each
         ! guard consumes a `pure` predicate next to its dispatcher and
         ! aborts once via `has_error` — same idiom as the lateral-closure
         ! guard above.  Every guard is inert on valid config (bit-identical).
         ! Coriolis form: a typo (→ PV_VARIANT_INVALID) or the
         ! reserved-but-unwired `al81` (→ PV_VARIANT_AL81) must NOT silently
         ! run enstrophy-only Sadourny — a different conservation law.
         if (.not. pv_variant_is_implemented( &
             parse_pv_variant(cfg%ocean%coriolis%form))) then
            call logger%error("&ocean_coriolis_nml form = '"// &
                              trim(cfg%ocean%coriolis%form)//"' is not implemented — "// &
                              "must be 'sadourny', 'sadourny_hk' or 'sadourny_energy' "// &
                              "('al81' is reserved but its Arakawa-Lamb kernel is not "// &
                              "yet wired)")
            has_error = .true.
         end if
         ! PV face-interpolation scheme: a typo (→ PV_ADV_INVALID) must not
         ! silently fall back to centred.
         if (.not. pv_adv_scheme_is_implemented( &
             parse_pv_adv_scheme(cfg%ocean%coriolis%pv_adv_scheme))) then
            call logger%error("&ocean_coriolis_nml pv_adv_scheme = '"// &
                              trim(cfg%ocean%coriolis%pv_adv_scheme)//"' is not recognised — "// &
                              "must be 'centered', 'weno3', 'weno5' or 'weno7'")
            has_error = .true.
         end if
         ! weno5/weno7 (stencil radius 3/4) need a halo of radius + 1 = 4/5
         ! (at radius alone a decomposed run is not bit-identical to one rank;
         ! see `pv_adv_required_nghost`) — fail-loud, mirroring the
         ! tracer-WENO ladder's per-rung nghost gate.
         if (cfg%nghost < pv_adv_required_nghost( &
             parse_pv_adv_scheme(cfg%ocean%coriolis%pv_adv_scheme))) then
            call logger%error("&ocean_coriolis_nml pv_adv_scheme='"// &
                              trim(cfg%ocean%coriolis%pv_adv_scheme)//"' requires nghost >= "// &
                              to_string(pv_adv_required_nghost( &
                                        parse_pv_adv_scheme(cfg%ocean%coriolis%pv_adv_scheme)))// &
                              " but nghost = "//to_string(cfg%nghost))
            has_error = .true.
         end if
         ! WENO PV reconstruction is wired only into the Sadourny enstrophy
         ! path; pairing it with the hk/energy forms would silently run the
         ! centred interpolation those kernels hard-code.
         if (trim(cfg%ocean%coriolis%pv_adv_scheme) /= "centered" .and. &
             trim(cfg%ocean%coriolis%form) /= "sadourny") then
            call logger%error("&ocean_coriolis_nml pv_adv_scheme='"// &
                              trim(cfg%ocean%coriolis%pv_adv_scheme)// &
                              "' is only wired into form='sadourny'; got form='"// &
                              trim(cfg%ocean%coriolis%form)//"'")
            has_error = .true.
         end if
         ! Mass-consistent CorAdCalc (use_state_fluxes): only the
         ! sadourny_energy transport form consumes uh/vh, and only the
         ! mom6 corrector has a predictor continuity solve to be
         ! consistent WITH — any other combination would silently run
         ! the recompute path while the namelist promised otherwise.
         if (cfg%ocean%coriolis%use_state_fluxes) then
            if (trim(cfg%ocean%coriolis%form) /= "sadourny_energy") then
               call logger%error("&ocean_coriolis_nml use_state_fluxes requires "// &
                                 "form='sadourny_energy' (the transport form is what "// &
                                 "consumes uh/vh); got form='"// &
                                 trim(cfg%ocean%coriolis%form)//"'")
               has_error = .true.
            end if
            if (trim(cfg%ocean%bt%split_scheme) /= "pred_corr") then
               call logger%error("&ocean_coriolis_nml use_state_fluxes requires "// &
                                 "&ocean_bt_nml split_scheme='pred_corr' (the corrector "// &
                                 "consumes the predictor chain's fluxes); got "// &
                                 "split_scheme='"//trim(cfg%ocean%bt%split_scheme)//"'")
               has_error = .true.
            end if
         end if
         ! BOUND_CORIOLIS clamps the ENERGY-scheme CAu (q·vh) to the
         ! (f+ζ)·v velocity-form range; it is wired into the energy impl
         ! only (MOM6 also applies it to the enstrophy scheme, but Roundabout's
         ! enstrophy path is a separate kernel — v1 covers the audit target).
         ! Silently ignoring it on another form would be a positivity-request
         ! foot-gun, so fail loud.
         if (cfg%ocean%coriolis%bound_coriolis .and. &
             trim(cfg%ocean%coriolis%form) /= "sadourny_energy") then
            call logger%error("&ocean_coriolis_nml bound_coriolis is implemented for "// &
                              "form='sadourny_energy' only (the energy-scheme q·vh blow-up "// &
                              "it cures); got form='"//trim(cfg%ocean%coriolis%form)//"'")
            has_error = .true.
         end if
         ! corner_h="mom6_area" is wired into the energy impl's Pass 2 only
         ! (MOM6 shares the q construction across schemes, but Roundabout's
         ! enstrophy/hk are separate kernels — v1 covers the audit target).
         if (trim(cfg%ocean%coriolis%corner_h) == "mom6_area" .and. &
             trim(cfg%ocean%coriolis%form) /= "sadourny_energy") then
            call logger%error("&ocean_coriolis_nml corner_h='mom6_area' is implemented for "// &
                              "form='sadourny_energy' only; got form='"// &
                              trim(cfg%ocean%coriolis%form)//"'")
            has_error = .true.
         end if
         ! Bottom-drag form: linear (τ=ρ·r·u) and quadratic (τ=ρ·C_d·|u|·u)
         ! obey different physics; a typo must abort, not silently pick the
         ! quadratic default.
         if (.not. bdrag_variant_is_implemented( &
             parse_bdrag_variant(cfg%ocean%bdrag%form))) then
            call logger%error("&ocean_bdrag_nml form = '"// &
                              trim(cfg%ocean%bdrag%form)//"' is not implemented — "// &
                              "must be 'linear'/'rayleigh' or 'quadratic'/'cd'")
            has_error = .true.
         end if
         ! gprime PGF is a 2-layer reduced-gravity form — it hard-writes
         ! ONLY the top+bottom layer, so nz/=2 silently zeros the PGF on the
         ! other layers.  Config-time check against nz_layers (not nz_ml).
         if (.not. gprime_nz_is_supported( &
             parse_opgf_variant(cfg%ocean%pgf%form), cfg%nz_layers)) then
            call logger%error("&ocean_pgf_nml form = 'gprime' requires "// &
                              "&nonhydrostatic_nml nz_layers = 2 (the reduced-gravity "// &
                              "form writes only the top + bottom layer; use "// &
                              "the default form='mont' for a general-nz PGF)")
            has_error = .true.
         end if
         ! Leith-biharmonic with c_leith_bi<=0 is a provable no-op (ν₄ is
         ! linear in c_leith_bi) — the user asked for biharmonic dissipation
         ! and got none.  Promoted from a configure-time warning to an abort.
         if (leith_biharm_is_inert(lateral_closure_code, cfg%ocean%hvisc%c_leith_bi)) then
            call logger%error("&ocean_hvisc_nml lateral_closure='leith_biharm' with "// &
                              "c_leith_bi <= 0 is inert (ν₄ is linear in c_leith_bi) — "// &
                              "set c_leith_bi > 0 or choose a different closure")
            has_error = .true.
         end if
         ! Tidal mixing enabled with no energy source (e_uniform<=0 and
         ! e_compute off) makes Kd ≡ 0 — the whole flux sweep runs for
         ! nothing.
         if (tidal_mixing_is_inert(cfg%ocean%tidal_mixing%enable, &
                                   cfg%ocean%tidal_mixing%e_uniform, &
                                   cfg%ocean%tidal_mixing%e_compute)) then
            call logger%error("&ocean_tidal_mixing_nml enable=.true. with e_uniform <= 0 "// &
                              "and e_compute=.false. supplies no energy — Kd is "// &
                              "identically zero; set e_uniform > 0 or e_compute=.true.")
            has_error = .true.
         end if
         ! OBC edge strings bypass the schema (external group), so a typo
         ! silently closed the boundary to a wall.  Reject INVALID per edge,
         ! naming which edge (the guard runs before configure_ocean_bc, so an
         ! invalid tag never reaches the setup path).
         if (ocean_bc_type_from_string(cfg%ocean%bc%west) == OBC_INVALID) then
            call logger%error("&ocean_bc_nml west = '"//trim(cfg%ocean%bc%west)// &
                              "' is not a recognised boundary type (wall/open/tidal/"// &
                              "nested/inflow/discharge/clamped/sponge/chapman/periodic/"// &
                              "tripolar_fold)")
            has_error = .true.
         end if
         if (ocean_bc_type_from_string(cfg%ocean%bc%east) == OBC_INVALID) then
            call logger%error("&ocean_bc_nml east = '"//trim(cfg%ocean%bc%east)// &
                              "' is not a recognised boundary type")
            has_error = .true.
         end if
         if (ocean_bc_type_from_string(cfg%ocean%bc%south) == OBC_INVALID) then
            call logger%error("&ocean_bc_nml south = '"//trim(cfg%ocean%bc%south)// &
                              "' is not a recognised boundary type")
            has_error = .true.
         end if
         if (ocean_bc_type_from_string(cfg%ocean%bc%north) == OBC_INVALID) then
            call logger%error("&ocean_bc_nml north = '"//trim(cfg%ocean%bc%north)// &
                              "' is not a recognised boundary type")
            has_error = .true.
         end if

         ! Along-coordinate tracer Laplacian (`&ocean_hdiff_nml kappa_h`):
         ! the explicit forward-Euler stability bound
         ! kappa_h*dt_therm*(1/dx^2+1/dy^2) <= 0.5 used to be checked HERE
         ! with the nominal `cfg%dx`/`cfg%dy` — broken on non-Cartesian
         ! grids (DEGREES there, not metres) and blind to the true minimum
         ! cell even on Cartesian.  MOVED to `ocean_stability_audit`
         ! (`rdb_ocean_stability_audit.F90`), which runs AFTER the real
         ! per-cell `ocean_metrics_t` arrays are built and works on every
         ! grid type.  See that module's docstring, or
         ! docs/CAPABILITIES_AND_LIMITATIONS.md, for the full check list.
      end if

      ! ---- Implicit vdiff stress/drag folding (mutual exclusions) ----
      ! `implicit_drag` (fold into the vdiff bed diagonal) is mutually
      ! exclusive with the legacy `&ocean_bdrag_nml implicit` split-apply
      ! path (both would damp the bed velocity ⇒ double drag), and the
      ! bed-only fold cannot represent HBBL-distributed drag (`hbbl > 0`)
      ! — that needs a per-layer rate (follow-up PR).  Fail loud.  (Not a
      ! `k = 1` problem: the fold and the HBBL band both start at the
      ! face's first live layer `k_bot_u/v`; what is missing is a 3-D
      ! `lambda_bot` the vdiff interior rows can add to their diagonal.)
      if (cfg%ocean%vdiff%implicit_drag .and. cfg%ocean%bdrag%implicit) then
         call logger%error("ocean_vdiff implicit_drag is mutually exclusive with "// &
                           "ocean_bdrag implicit (split-apply): set only one")
         has_error = .true.
      end if
      if (cfg%ocean%vdiff%implicit_drag .and. cfg%ocean%bdrag%hbbl > 0.0_wp &
          .and. .not. bbl_glue_is_effective(cfg)) then
         ! Exempt only while the glue is EFFECTIVE (it carries the band):
         ! a requested glue that setup turns off (no drag coefficient)
         ! would otherwise leave the unsupported fold + band running.
         call logger%error("ocean_vdiff implicit_drag does not yet support "// &
                           "HBBL-distributed drag (ocean_bdrag hbbl > 0); use the "// &
                           "split-apply path (ocean_bdrag implicit) for HBBL, or "// &
                           "bbl_glue with a nonzero bottom drag (quadratic cd > 0, "// &
                           "or linear r > 0 with bg_vel > 0)")
         has_error = .true.
      end if
      ! `implicit_top_drag` is the `k = nz` twin of the rule above, plus
      ! one of its own: the surface row is the row the WIND stress owns
      ! as a Neumann RHS, so the fold both adds a diagonal term and
      ! masks that RHS off on the faces the ice covers.  That is only
      ! meaningful with a top drag configured.
      if (cfg%ocean%vdiff%implicit_top_drag) then
         if (.not. cfg%ocean%tdrag%enable) then
            call logger%error("&ocean_vdiff_nml implicit_top_drag=.true. requires "// &
                              "&ocean_tdrag_nml enable=.true.  The fold consumes the "// &
                              "top-drag slot's lambda_top_u/v and its face cover "// &
                              "masks; with the slot disabled those are placeholder "// &
                              "arrays and the knob would silently do nothing except "// &
                              "look like a top drag was configured.")
            has_error = .true.
         end if
         if (cfg%ocean%tdrag%implicit) then
            call logger%error("&ocean_vdiff_nml implicit_top_drag is mutually "// &
                              "exclusive with &ocean_tdrag_nml implicit: both damp "// &
                              "the top layer, so running both is a DOUBLE COUNT, not "// &
                              "a stronger drag.  Pick one — the vdiff fold if the "// &
                              "column also has real vertical viscosity to couple "// &
                              "against, the in-kernel backward-Euler form otherwise.")
            has_error = .true.
         end if
         if (cfg%ocean%tdrag%htbl > 0.0_wp) then
            call logger%error("&ocean_vdiff_nml implicit_top_drag does not support "// &
                              "the HTBL-distributed top drag (&ocean_tdrag_nml htbl "// &
                              "> 0): the fold is a SINGLE k = nz Rayleigh rate on the "// &
                              "diagonal and cannot represent a band spread over "// &
                              "several layers (the mirror of the implicit_drag/HBBL "// &
                              "restriction).  Use &ocean_tdrag_nml implicit for a "// &
                              "distributed top drag.")
            has_error = .true.
         end if
      end if
      ! `bbl_glue` (MOM6 BOTTOMDRAGLAW) needs the height-above-bed stack
      ! that only the hvel_mom6 path accumulates.  It composes with every
      ! drag form and fold: its piston IS the bed sink (the explicit apply,
      ! the `&ocean_bdrag_nml implicit` split-apply and the `implicit_drag`
      ! fold are all skipped on the layers; the explicit tendency still
      ! feeds the barotropic F_slow, as under `implicit_drag`), which is
      ! also why `implicit_drag` + `hbbl > 0` is accepted under it.
      if (cfg%ocean%vdiff%bbl_glue) then
         if (.not. cfg%ocean%vdiff%hvel_mom6) then
            call logger%error("ocean_vdiff bbl_glue requires hvel_mom6=.true. — the "// &
                              "botfn glue reads the height-above-bed stack that only "// &
                              "the hvel_mom6 face-thickness build accumulates")
            has_error = .true.
         end if
      end if
      ! `implicit_stress` injects the wind stress at the surface row (k=nz)
      ! only — it cannot represent the DIRECT_STRESS distribution of stress
      ! over the top `hmix_stress` metres (`vmix%direct_stress`).  Enabling
      ! both would silently drop the distribution (the fold wins).  Symmetric
      ! to the implicit_drag/HBBL guard above.  Fail loud.
      if (cfg%ocean%vdiff%implicit_stress .and. cfg%ocean%vmix%direct_stress) then
         call logger%error("ocean_vdiff implicit_stress is incompatible with "// &
                           "distributed wind stress (ocean_vmix direct_stress): the "// &
                           "surface-row fold cannot spread stress over hmix_stress")
         has_error = .true.
      end if

      ! RETIRED `correction_h_weighted`: the h-weighted barotropic-
      ! correction fold is not energy-conserving on any column whose
      ! (open) layers differ in thickness — which is every column of a
      ! stretched z_fixed stack — and MOM6 has no such fold.  Refused,
      ! never silently ignored, so a namelist that relied on it learns
      ! that its answer changes.  See `apply_bt_correction`.
      if (cfg%ocean%bt%correction_h_weighted) then
         ! Pushed to the error ring as well as logged, so a C/Python
         ! caller (and `test_ocean_bt_correction_weight`) reads the
         ! specific reason, not only the generic validation rollup.
         block
            character(len=*), parameter :: msg = &
                                           "&ocean_bt_nml correction_h_weighted is RETIRED: the h-weighted "// &
                                           "barotropic-correction fold was energy-non-conserving (beyond the "// &
                                           "barotropic KE change it adds a positive source 0.5*D^2*H*(kappa-1), "// &
                                           "kappa = sum(h^3)sum(h)/sum(h^2)^2 >= 1, plus a shear feedback that grew "// &
                                           "stretched z_fixed runs non-finite). MOM6 has no h-weighted fold; the "// &
                                           "uniform fold is the default and MOM6's own one (accel_layer_u applies "// &
                                           "the BT acceleration uniformly). For drag-aware damping use "// &
                                           "visc_rem_chain=.true. (with &ocean_vdiff_nml implicit_drag or "// &
                                           "bbl_glue) -- it does NOT re-weight this fold (correction_visc_rem, "// &
                                           "the knob that used to, is itself retired), it feeds visc_rem into "// &
                                           "bt_rem/av_rem instead. Else delete the key."
            call error_ring_push(msg)
            call logger%error(msg)
         end block
         has_error = .true.
      end if
      ! The vdiff operator's row sums are exactly 1 (a no-flux-top/no-flux-
      ! bottom viscous operator cannot remove a uniform acceleration)
      ! UNLESS the implicit-drag fold breaks the k=1 row sum.  So without
      ! `implicit_drag` (or `bbl_glue`), the remnant producer still runs
      ! but returns gamma ≡ 1 identically, and every `*_visc_rem` consumer
      ! degenerates to its own no-op (the forcing weight to the plain
      ! h-mean, the continuity renormaliser to the uniform `du`, av_rem to
      ! 1).  Legal and mathematically correct — merely inert.  Warn (not
      ! error): same "enabled but inert" precedent as the tidal-mixing
      ! e_uniform=0 warning.  PR-3 (D1) follow-up: `forcing_visc_rem`/
      ! `renorm_visc_rem`/`bt_rem_from_visc_rem` are each now
      ! SELF-SUFFICIENT — the producer is decoupled from the retired
      ! `correction_visc_rem` weighted fold and instead runs whenever ANY
      ! of them (or `visc_rem_chain`) is on, so none of them "requires" a
      ! separate producer knob any more.
      if (ocean_bt_visc_rem_producer_on(cfg) .and. .not. &
          (cfg%ocean%vdiff%implicit_drag .or. cfg%ocean%vdiff%bbl_glue)) then
         call logger%warning("&ocean_bt_nml forcing_visc_rem/renorm_visc_rem/"// &
                             "bt_rem_from_visc_rem/visc_rem_chain is on but neither "// &
                             "&ocean_vdiff_nml implicit_drag nor bbl_glue is: the vdiff "// &
                             "operator then carries no drag, so visc_rem = 1 identically "// &
                             "and every consumer is a no-op")
      end if
      ! The bc-PGF retro-correction (MOM6 btstep_layer_accel) builds its
      ! per-layer `pbce` from the FV_MOM6 interface-height stack
      ! `pgf%e_face`, which no other form fills.  It used to be ACCEPTED here
      ! and `error stop` inside step 1 (`compute_pbce`); refuse it at
      ! configure, naming the same requirement.  Not generalised: the
      ! MONT / FV_LITE / FV_WRIGHT forms are surface-relative (they carry no
      ! free-surface term, `g_pf = 0`), so the response `pbce = dp_k/deta`
      ! the correction redistributes is not the one their PGF sees, and
      ! GPRIME runs the fast loop at the reduced `g_FS`.
      if (cfg%ocean%bt%correction_bc_pgf .and. &
          parse_opgf_variant(cfg%ocean%pgf%form) /= OPGF_VARIANT_FV_MOM6) then
         call logger%error("&ocean_bt_nml correction_bc_pgf=.true. requires "// &
                           "&ocean_pgf_nml form='fv_mom6' (got '"// &
                           trim(adjustl(cfg%ocean%pgf%form))//"'). compute_pbce "// &
                           "builds the per-layer pressure response from the FV_MOM6 "// &
                           "interface-height stack (pgf%e_face), which no other PGF "// &
                           "form fills.")
         has_error = .true.
      end if
      ! pred_corr (SPEC S4) envelope: any ALE / Lagrangian-within-step
      ! vcoord (lagrangian, sigma, zstar, zsigma, zstar_sigma,
      ! zstar_full) — MOM6's own model: the dynamics step is always
      ! Lagrangian, regridding is orthogonal, and the pc corrector flows
      ! into the same thermo-cadence ALE remap the ssp path uses.  The
      ! LEGACY pure Eulerian-z path stays excluded: its per-stage
      ! vertical advection + BT-fold h-rescale would compose differently
      ! inside the restructured loop (the fold's rescale lands BEFORE the
      ! deferred continuity — a double eta application) — untested, fail
      ! loud.  Also still excluded: dynamic wet/dry (per-stage masking
      ! composes with the ssp stages only) and windowed tracer advection
      ! (the predictor's TR_MODE_NONE + window accounting is unexercised).
      if (trim(cfg%ocean%bt%split_scheme) == "pred_corr") then
         if (parse_vcoord_type(cfg%vcoord_type, &
                               default_code=VCOORD_EULERIAN_Z) == VCOORD_EULERIAN_Z) then
            call logger%error("&ocean_bt_nml split_scheme='pred_corr' requires an ALE "// &
                              "vertical coordinate (lagrangian/sigma/zstar/zsigma/"// &
                              "zstar_sigma/zstar_full) — the legacy 'eulerian_z' "// &
                              "per-stage vertical-advection + h-rescale path is not "// &
                              "wired through the restructured pc loop; got '"// &
                              trim(cfg%vcoord_type)//"'. pred_corr is the DEFAULT, so "// &
                              "set split_scheme='ssp_rk2' to keep this configuration.")
            has_error = .true.
         end if
         if (cfg%ocean%wetdry%enable) then
            call logger%error("&ocean_bt_nml split_scheme='pred_corr' is incompatible "// &
                              "with &ocean_wetdry_nml enable (v1 envelope). pred_corr "// &
                              "is the DEFAULT, so set split_scheme='ssp_rk2' to keep "// &
                              "this configuration.")
            has_error = .true.
         end if
         if (cfg%ocean%vmix%dt_tracer_advect_ratio > 1) then
            call logger%error("&ocean_bt_nml split_scheme='pred_corr' requires "// &
                              "dt_tracer_advect_ratio = 1 (v1 envelope). pred_corr is "// &
                              "the DEFAULT, so set split_scheme='ssp_rk2' to keep this "// &
                              "configuration.")
            has_error = .true.
         end if
         ! z-level closed faces need their SOLID WALLS to be walls.  With
         ! `mask_wall_velocity = .false.` a wall face keeps a free per-layer
         ! velocity that carries no mass (continuity zeroes the wall flux)
         ! and whose depth mean the BT fold resets to zero every stage —
         ! but whose BAROCLINIC part nothing restores under pred_corr: the
         ! Coriolis reads `u_av`, which the transport renormaliser never
         ! writes at a skipped wall face, so the wall velocity never sees
         ! its own rotation and integrates the layer Coriolis of its
         ! interior neighbour without bound.  The closed-face mask is what
         ! makes that neighbour baroclinic (a closed bed layer beside open
         ! ones).  Measured on the rotating ledge basin of
         ! tests/test_ocean_zfixed_cor_ref.F90: KE+PE x1783 over 4000
         ! outer steps with the physical interior at x1.24 — the whole
         ! growth sits on the unmasked wall faces — against x0.74 with
         ! the walls masked.  The mask is the DEFAULT; refused rather than
         ! silently overridden.
         if (cfg%zfixed_closed_faces .and. .not. cfg%ocean%bc%mask_wall_velocity) then
            call logger%error("&ocean_bt_nml split_scheme='pred_corr' with "// &
                              "&vcoord_nml zfixed_closed_faces=.true. requires "// &
                              "&ocean_bc_nml mask_wall_velocity=.true. (the default): "// &
                              "an unmasked solid-wall face carries a baroclinic layer "// &
                              "velocity that pred_corr's u_av-evaluated Coriolis never "// &
                              "rotates, so it grows without bound. Set "// &
                              "mask_wall_velocity=.true., or split_scheme='ssp_rk2'.")
            has_error = .true.
         end if
      end if
      ! RETIRED `accel_visc_rem`: PR-3's audit found no MOM6 state-update
      ! equivalent — `btstep_layer_accel` and the split-explicit RK2
      ! corrector's `up`/`vp` update both apply the depth-mean
      ! barotropic acceleration `u_accel_bt` UNIFORMLY across every
      ! layer — no `visc_rem` weight anywhere in that path (the only
      ! `visc_rem x u_accel_bt` products in MOM6 are a diagnostic-only
      ! `id_u_BT_accel_visc_rem` post-product, never fed back into state,
      ! and `RESCALE_STRONG_DRAG`'s depth-MEAN, not per-layer, rescale —
      ! already its own knob).  The real MOM6 mechanisms that multiply a
      ! velocity correction by `visc_rem` are `renorm_visc_rem` (continuity
      ! `u_cor = u + du*visc_rem`) and `rescale_strong_drag`.  Refused,
      ! never silently ignored.
      if (cfg%ocean%vdiff%accel_visc_rem) then
         block
            character(len=*), parameter :: msg = &
                                           "&ocean_vdiff_nml accel_visc_rem is RETIRED: PR-3's audit found no "// &
                                           "MOM6 state-update equivalent -- MOM6's btstep_layer_accel applies "// &
                                           "the depth-mean barotropic acceleration uniformly across every "// &
                                           "layer, with no visc_rem weight. The real MOM6 mechanisms that "// &
                                           "multiply a velocity correction by visc_rem are "// &
                                           "&ocean_bt_nml renorm_visc_rem (continuity u_cor = u + du*visc_rem) "// &
                                           "and rescale_strong_drag. Use one of those, or delete the key."
            call error_ring_push(msg)
            call logger%error(msg)
         end block
         has_error = .true.
      end if
      ! RETIRED `correction_visc_rem`: MOM6's `accel_layer_u` gives every
      ! layer the SAME `u_accel_bt` plus only the depth-mean-zero `pbce`
      ! baroclinic-pressure term — NO `visc_rem` weight — and that
      ! unweighted acceleration is folded into `up` BEFORE `vertvisc`
      ! (in the split-explicit RK2 corrector), so the glue's implicit
      ! friction (which already includes the BBL
      ! piston) is what then distributes it across layers — ONCE, not
      ! twice.  roundabout's `correction_visc_rem` applies
      ! `apply_bt_correction` BEFORE that same implicit friction
      ! (`vmix_apply_in_stage`) and weights it by `visc_rem_k/⟨visc_rem⟩_h`
      ! on top — a SECOND, unbounded-ratio damping that concentrates the
      ! correction into whichever layers the glue left least damped.
      ! Measured on the 1-degree Southern Ocean z* OPEN-step probe
      ! (`probes/pr3_producer_only`): `hvel_mom6`+`bbl_glue`+
      ! `implicit_drag` alone runs clean; adding ONLY `correction_visc_rem`
      ! NaNs at step 38.  Under D1 ("exactly MOM6's set") this fold is not
      ! part of the chain and has no standalone namelist path of its own;
      ! refused, never silently ignored.  The kernel itself
      ! (`apply_bt_correction`'s `use_visc_rem` dispatch) and its direct
      ! unit tests (`tests/test_ocean_bt_correction_weight.F90`
      ! `visc_rem_unity_is_uniform`/`visc_rem_biases_against_bed`/
      ! `closed_faces_open_column`) are untouched — they call it with a
      ! raw logical, not through `cfg`.
      if (cfg%ocean%bt%correction_visc_rem) then
         block
            character(len=*), parameter :: msg = &
                                           "&ocean_bt_nml correction_visc_rem is RETIRED: MOM6's accel_layer_u "// &
                                           "applies the barotropic acceleration UNIFORMLY across every layer, "// &
                                           "before vertvisc distributes it via "// &
                                           "the SAME implicit friction the glue uses -- this fold re-weights it "// &
                                           "a second time by visc_rem/<visc_rem>_h, an unbounded ratio that "// &
                                           "NaNs the 1-degree Southern Ocean z* open-step case under bbl_glue "// &
                                           "at step ~40. Use visc_rem_chain (producer + bt_rem_from_av_rem + "// &
                                           "wt_u forcing + renorm_visc_rem, uniform BT-correction fold) instead, "// &
                                           "or delete the key."
            call error_ring_push(msg)
            call logger%error(msg)
         end block
         has_error = .true.
      end if
      ! D2: `substep_drag`'s linear piston and the visc_rem chain both put
      ! bed drag into bt_rem — composing them double-counts it (once via
      ! the glue/implicit_drag fold inside visc_rem, once via the piston).
      if (ocean_bt_rem_from_visc_rem_on(cfg) .and. cfg%ocean%bt%substep_drag) then
         call logger%error("&ocean_bt_nml bt_rem_from_visc_rem=.true. (or "// &
                           "visc_rem_chain=.true.) is mutually exclusive with "// &
                           "substep_drag=.true. (bed drag would be double-counted: once "// &
                           "inside the visc_rem producer via the bbl_glue/implicit_drag "// &
                           "fold, once again via the linear piston law)")
         has_error = .true.
      end if
      ! Decomposition: av_rem/bt_rem are built on the SAME normal-width
      ! face stencil visc_rem occupies (valid after PR-1's halo refresh);
      ! the wide-halo BT clone's metrics_w/halo-widened arrays carry no
      ! av_rem/visc_rem ghost width yet — same posture as porous.
      if (ocean_bt_rem_from_visc_rem_on(cfg) .and. cfg%ocean%bt%bt_halo > 0) then
         call logger%error("&ocean_bt_nml bt_rem_from_visc_rem=.true. (or "// &
                           "visc_rem_chain=.true.) is mutually exclusive with "// &
                           "bt_halo > 0 (the wide-halo BT clone carries no "// &
                           "av_rem/visc_rem ghost-width statistics, like porous)")
         has_error = .true.
      end if
      ! D3: BT_STRONG_DRAG / RESCALE_STRONG_DRAG are refinements OF the
      ! av_rem chain, not independent knobs.
      if (cfg%ocean%bt%strong_drag .and. .not. ocean_bt_rem_from_visc_rem_on(cfg)) then
         call logger%error("&ocean_bt_nml strong_drag=.true. requires "// &
                           "bt_rem_from_visc_rem=.true. (or visc_rem_chain=.true.) — "// &
                           "the rational-approximation bt_rem form is only defined in "// &
                           "terms of av_rem")
         has_error = .true.
      end if
      if (cfg%ocean%bt%rescale_strong_drag .and. .not. cfg%ocean%bt%strong_drag) then
         call logger%error("&ocean_bt_nml rescale_strong_drag=.true. requires "// &
                           "strong_drag=.true. — the rescale corrects for the rational "// &
                           "form's bt_rem**n_inner /= av_rem gap, which the plain power "// &
                           "form does not have")
         has_error = .true.
      end if
      if (substep_drag_ignores_bdrag_form(cfg)) then
         call logger%warning("&ocean_bt_nml substep_drag=.true. with "// &
                             "&ocean_bdrag_nml form='"// &
                             trim(adjustl(cfg%ocean%bdrag%form))//"': the "// &
                             "barotropic substep damping is built from the "// &
                             "LINEAR coefficient &ocean_bdrag_nml r (times hbbl) "// &
                             "only, so it does NOT follow this bottom drag — "// &
                             "with r = 0 (the default) substep_drag is a no-op, "// &
                             "otherwise it damps the barotropic mode with a "// &
                             "linear drag the slow step never applies.  Use "// &
                             "form='linear', or drop substep_drag")
      end if

      ! Equilibrium tide (C1) requires lat/lon — meaningless on a
      ! cartesian grid (geolatT/geolonT stay 0).  Fail loud rather than
      ! silently forcing a flat basin.
      if (cfg%ocean%tides%enable .and. &
          trim(cfg%ocean%grid%grid_config) == "cartesian") then
         call logger%error("ocean_tides_nml: enable=.true. requires a "// &
                           "non-cartesian grid_config (spherical/tripolar/"// &
                           "supergrid) — the equilibrium tide needs lat/lon")
         has_error = .true.
      end if
      if (cfg%ocean%tides%enable .and. len_trim(cfg%ocean%tides%constituents) == 0) then
         call logger%error("ocean_tides_nml: enable=.true. but constituents "// &
                           "list is empty")
         has_error = .true.
      end if

      ! C7 Henyey latitude factor: copy of the equilibrium-tide cartesian
      ! guard above.  `geolatT` is identically zero on a cartesian grid (only
      ! the spherical/supergrid/tripolar fills populate it), so EVERY column
      ! would take the equatorial factor L(0 deg) = 0 and the background
      ! would collapse to a uniform `bkgnd_kd_min` everywhere while the
      ! console cheerfully logged "Henyey IGW latitude factor ON".  The
      ! `kd_min` floor makes that less catastrophic than the unfloored
      ! zeroing it used to be, but it is still a latitude parameterisation
      ! on a grid with no meaningful latitude — strictly worse than leaving
      ! the knob off, so refuse at configure.
      if (cfg%ocean%vmix%bkgnd_henyey .and. &
          trim(cfg%ocean%grid%grid_config) == "cartesian") then
         call logger%error("&ocean_vmix_nml bkgnd_henyey=.true. requires a "// &
                           "non-cartesian &ocean_grid_nml grid_config (spherical/"// &
                           "tripolar/supergrid) — geolatT is meaningless on cartesian, "// &
                           "so every column would take the equatorial L(0 deg)=0 and "// &
                           "the background would flatten to a uniform bkgnd_kd_min")
         has_error = .true.
      end if
      ! Bryan-Lewis and Henyey are MUTUALLY EXCLUSIVE background schemes,
      ! matching the reference code, which FATALs when a second background
      ! scheme is selected.  Henyey scales the SCALAR background; letting it
      ! also multiply the Bryan-Lewis deep asymptote would suppress abyssal /
      ! internal-tide mixing at the equator, which is not what the Henyey
      ! scaling describes.
      if (bkgnd_henyey_conflicts_profile(cfg%ocean%vmix%bkgnd_henyey, &
                                         cfg%ocean%vmix%bkgnd_profile)) then
         call logger%error("&ocean_vmix_nml bkgnd_henyey and bkgnd_profile are "// &
                           "mutually exclusive background schemes — pick ONE "// &
                           "(Bryan-Lewis depth profile, or the Henyey latitude "// &
                           "factor on the scalar background)")
         has_error = .true.
      end if
      ! Surface-pressure loading / inverse barometer (PR-17).  The seam it
      ! folds into (`eta_forcing`) lives in the barotropic substep, so the
      ! feature is split-solver only; the `p_surf` field it reads is
      ! allocated only with the PR-12 component set.  Every violation fails
      ! loud rather than silently reading an unallocated array (a silent
      ! stale device read on the mem:separate GPU build).
      if (cfg%ocean%psurf%enable) then
         if (.not. cfg%ocean%forcing%enable_components) then
            call logger%error("&ocean_psurf_nml enable=.true. requires "// &
                              "&ocean_forcing_nml enable_components=.true. "// &
                              "(p_surf is allocated only with the component set)")
            has_error = .true.
         end if
         if (cfg%ocean%bt%n_inner < 1 .and. .not. cfg%ocean%bt%auto_n_inner) then
            call logger%error("&ocean_psurf_nml enable=.true. requires the "// &
                              "split-explicit solver (&ocean_bt_nml n_inner >= 1 "// &
                              "or auto_n_inner=.true.) — the eta_forcing seam "// &
                              "lives in the barotropic substep")
            has_error = .true.
         end if
         ! A UNIFORM surface pressure has no gradient => provably inert
         ! (gauge invariance).  A user enabling p_surf with only a uniform
         ! constant and no file/override path gets nothing — say so rather
         ! than repeat the &ocean_tidal_mixing_nml e_uniform silent-no-op.
         if (cfg%ocean%psurf%p_surf_const /= 0.0_wp) then
            if (cfg%ocean%psurf%in_eos) then
               ! With in_eos the gauge argument does NOT apply to the EOS:
               ! it is nonlinear in pressure, so a spatially UNIFORM load
               ! still changes the IN-SITU densities it reaches.  Say so
               ! instead of the (then wrong) "provably inert" warning.
               call logger%info("&ocean_psurf_nml in_eos=.true.: a uniform "// &
                                "p_surf_const is inert on the barotropic seam "// &
                                "(only grad(p_surf) is physical there) but NOT "// &
                                "in the IN-SITU EOS pressure — it compresses "// &
                                "the water. The potential density ms%rho_layer "// &
                                "is unaffected either way (uniform p_ref).")
            else
               call logger%warning("&ocean_psurf_nml enable=.true. with a uniform "// &
                                   "p_surf_const and no file/override path: only "// &
                                   "grad(p_surf) is physical, so a spatially "// &
                                   "constant load is inert (gauge invariance). "// &
                                   "File-driven p_surf lands in PR-14/PR-15.")
            end if
         end if
      end if
      ! ---- Top-of-column pressure in the IN-SITU EOS arguments (E3) ----
      ! `in_eos` offsets the EOS's IN-SITU pressure arguments by
      ! `ms%p_top`.  v1 ports exactly one: the FV_WRIGHT Picard column
      ! sweep.  It does NOT touch `ms%rho_layer`, which is a potential
      ! density at the horizontally uniform `&ocean_eos_nml p_ref` and must
      ! stay that way.  Every OTHER in-situ consumer builds its own
      ! surface-relative hydrostatic pressure starting at 0 Pa at the free
      ! surface and has NOT been ported; running them against a loaded
      ! column would mix two incompatible pressure conventions inside one
      ! time step with no symptom.  Refuse fail-loud rather than be
      ! silently inconsistent — each line below is a named follow-up, not a
      ! permanent limit.
      if (cfg%ocean%psurf%in_eos) then
         ! Inert-configuration warning, not a refusal: with no in-situ
         ! consumer selected the knob legitimately does nothing, and the
         ! house rule (cf. &ocean_tidal_mixing_nml e_uniform) is to SAY so
         ! rather than let a user believe a cavity load reached the EOS.
         ! E4 adds a THIRD ported consumer: `buoyancy_coeffs="eos"` seeds
         ! the KPP B_0 coefficients at `p_top` and the double-diffusion
         ! interface stack from it, so the knob is no longer inert when
         ! that is selected.
         if (trim(adjustl(cfg%ocean%pgf%form)) /= "fv_wright" .and. &
             .not. cfg%ocean%epbl%enable .and. &
             trim(adjustl(cfg%ocean%vmix%buoyancy_coeffs)) /= "eos") then
            call logger%warning("&ocean_psurf_nml in_eos=.true. is INERT for "// &
                                "&ocean_pgf_nml form='"// &
                                trim(adjustl(cfg%ocean%pgf%form))//"' with no "// &
                                "other ported in-situ consumer: the ported ones "// &
                                "are the FV_WRIGHT Picard column sweep, the "// &
                                "EPBL column stack (&ocean_epbl_nml) and the "// &
                                "EOS-derived buoyancy coefficients "// &
                                "(&ocean_vmix_nml buoyancy_coeffs='eos'). The "// &
                                "other PGF forms read the POTENTIAL density "// &
                                "ms%rho_layer, which is referenced to the "// &
                                "uniform &ocean_eos_nml p_ref BY DESIGN and is "// &
                                "not offset by the load.")
         end if
         if (.not. cfg%ocean%psurf%enable) then
            call logger%error("&ocean_psurf_nml in_eos=.true. requires "// &
                              "enable=.true. (p_top is filled from the "// &
                              "assembled sf%p_surf the seam owns)")
            has_error = .true.
         end if
         ! EPBL was refused here until Phase 4b.  It is PORTED now:
         ! `epbl_column_kernel` seeds its stack at `ms%p_top(i,j)` when
         ! `in_eos`, which moves BOTH consumers of that stack (the
         ! in-situ `eos_specvol_derivs` argument and the PE weight
         ! `dmass*p_mid*dsv`) together.  Gate:
         ! `test_ocean_bl_under_ice` (`epbl_p_top_*`).
         if (cfg%ocean%kshear%enable) then
            call logger%error("&ocean_psurf_nml in_eos=.true. is not supported "// &
                              "with &ocean_kappa_shear_nml enable=.true. — "// &
                              "ks_solve_column builds its own interface pressure "// &
                              "from 0 Pa at the surface; not yet ported to p_top")
            has_error = .true.
         end if
         if (cfg%ocean%tidal_mixing%enable) then
            call logger%error("&ocean_psurf_nml in_eos=.true. is not supported "// &
                              "with &ocean_tidal_mixing_nml enable=.true. — "// &
                              "tidal_mixing_column_kernel builds its own interface "// &
                              "pressure from 0 Pa at the surface; not yet ported")
            has_error = .true.
         end if
         if (cfg%ocean%redi%enable) then
            call logger%error("&ocean_psurf_nml in_eos=.true. is not supported "// &
                              "with &ocean_redi_nml enable=.true. — "// &
                              "redi_build_column seeds Pint(1)=0 at the surface; "// &
                              "not yet ported to p_top")
            has_error = .true.
         end if
         if (cfg%ocean%slopes%enable) then
            call logger%error("&ocean_psurf_nml in_eos=.true. is not supported "// &
                              "with &ocean_slopes_nml enable=.true. — "// &
                              "pressure_above_x sums g*rho0*h down from 0 Pa at "// &
                              "the surface; not yet ported to p_top")
            has_error = .true.
         end if
         if (cfg%ocean%pgf%reconstruct_for_pressure) then
            call logger%error("&ocean_psurf_nml in_eos=.true. is not supported "// &
                              "with &ocean_pgf_nml reconstruct_for_pressure=.true. "// &
                              "— boole_dpa_intz_layer builds the EOS pressure as "// &
                              "p = -g*rho0*z from the surface-relative interface "// &
                              "height; not yet ported to p_top")
            has_error = .true.
         end if
         if (trim(adjustl(cfg%ocean%pgf%form)) == "fv_mom6" .and. &
             .not. cfg%ocean%pgf%reconstruct_for_pressure .and. &
             cfg%ocean%pgf%insitu_density .and. &
             trim(adjustl(cfg%ocean%eos%eos)) /= "linear") then
            call logger%error("&ocean_psurf_nml in_eos=.true. is not supported "// &
                              "with the FV_MOM6 in-situ PCM density (&ocean_pgf_nml "// &
                              "insitu_density=.true., the default, under a "// &
                              "pressure-dependent EOS) — boole_dpa_intz_layer builds "// &
                              "the EOS pressure as p = -g*rho0*z; not yet ported to "// &
                              "p_top.  Set insitu_density=.false. for the legacy "// &
                              "potential-density integral")
            has_error = .true.
         end if
         if (cfg%ocean%ice%enable) then
            call logger%error("&ocean_psurf_nml in_eos=.true. is not supported "// &
                              "with &ocean_ice_nml enable=.true. — the freezing "// &
                              "point is evaluated at p=0 in the frazil / basal-flux "// &
                              "kernels, and the liquidus pressure depression is "// &
                              "exactly what an ice-shelf load changes; not yet ported")
            has_error = .true.
         end if
      end if
      ! ---- Z-level T/S initial-condition overlay (`&ocean_zinit_nml`) ----
      if (cfg%ocean%zinit%enable) then
         select case (trim(adjustl(cfg%ocean%zinit%source)))
         case ("file")
            if (len_trim(cfg%ocean%zinit%file) == 0) then
               call logger%error("&ocean_zinit_nml enable=.true. with "// &
                                 "source='file' requires a non-blank file= path.")
               has_error = .true.
            end if
         case ("linear")
            ! An analytic profile plus a file path is ambiguous: the file
            ! would be silently ignored.  Say so rather than pick one.
            if (len_trim(cfg%ocean%zinit%file) > 0) then
               call logger%error("&ocean_zinit_nml source='linear' takes the "// &
                                 "analytic lin_* profile and opens NOTHING, so the "// &
                                 "file='"//trim(adjustl(cfg%ocean%zinit%file))// &
                                 "' you also set would be silently ignored.  Pick "// &
                                 "one: drop file=, or set source='file'.")
               has_error = .true.
            end if
         case default
            call logger%error("&ocean_zinit_nml source='"// &
                              trim(adjustl(cfg%ocean%zinit%source))// &
                              "' is not a known profile source; expected 'file' "// &
                              "(pre-regridded NetCDF) or 'linear' (analytic "// &
                              "affine T(z)/S(z)).")
            has_error = .true.
         end select
      end if
      ! ---- Top-of-column load in the PGF surface boundary condition (P5.0) ----
      ! `pa(nz+1) = rho_ref*g*eta_geo + ms%p_top`.  Only the FV_MOM6 family
      ! builds a `pa` stack at all — `mont` hard-zeroes `M(nz)` and
      ! `fv_lite`/`fv_wright` seed `p_edge(nz+1) = 0` — so there is
      ! literally no boundary condition to inject anywhere else.  Refuse
      ! rather than accept a knob that would silently do nothing on a form
      ! the user believes is carrying an ice load.
      if (cfg%ocean%pgf%p_top_in_bc) then
         if (trim(adjustl(cfg%ocean%pgf%form)) /= "fv_mom6") then
            call logger%error("&ocean_pgf_nml p_top_in_bc=.true. requires "// &
                              "form='fv_mom6' (got '"// &
                              trim(adjustl(cfg%ocean%pgf%form))//"'). Only the "// &
                              "FV_MOM6 family builds the pa(nz+1) pressure-stack "// &
                              "boundary condition the load is injected into; mont "// &
                              "hard-zeroes M(nz) and fv_lite/fv_wright seed "// &
                              "p_edge(nz+1)=0.")
            has_error = .true.
         end if
         ! Inert-configuration warning, not a refusal (the house rule, cf.
         ! &ocean_psurf_nml in_eos and &ocean_tidal_mixing_nml e_uniform).
         ! `ms%p_top = metrics%p_ice_ref + sf%p_surf` has TWO producers,
         ! and the warning must name both or it is a lie: the
         ! &ocean_psurf_nml seam (the atmospheric half, `sf%p_surf`) and
         ! the &ocean_cavity_dyn_nml ice-shelf load (the static half,
         ! `p_ice_ref = rho_ref*g*z_draft`, assembled in
         ! `configure_ocean_cavity`).  Under a cavity `p_top` carries the
         ! ice load and `p_top_in_bc` is not merely live — it is REQUIRED
         ! for a varying draft, refused above and again at configure.  The
         ! warning fires only when NEITHER producer is on, which is the
         ! one case in which `p_top` really is the zero array it was
         ! allocated as.
         if (.not. p_top_has_producer(cfg)) then
            call logger%warning("&ocean_pgf_nml p_top_in_bc=.true. is INERT "// &
                                "without a producer for ms%p_top: enable "// &
                                "&ocean_psurf_nml (the atmospheric surface-pressure "// &
                                "seam) or &ocean_cavity_dyn_nml (the static "// &
                                "ice-shelf load), else p_top is the zero array and "// &
                                "pa(nz+1) is unchanged.")
         end if
      end if
      ! ---- Static ice-shelf cavity geometry (&ocean_cavity_dyn_nml, P5.1) ----
      ! The draft is absorbed into the barotropic DATUM (bt_H_ref =
      ! b - z_draft), so every consumer of the water-column thickness
      ! D = bt_H_ref + bt_eta is correct with no cavity branch of its own.
      ! What that buys is paid for by a narrow envelope, and EVERY
      ! restriction below fails loud naming the knob and the reason: a
      ! cavity that silently runs outside it looks plausible and is wrong
      ! (a coordinate anchored at z = 0 under 500 m of ice, a second
      ! un-reconciled surface load, a wide-halo BT clone with no draft).
      if (cfg%ocean%cavity_dyn%trim_ic_for_p_surf .and. &
          .not. cfg%ocean%cavity_dyn%enable) then
         call logger%error("&ocean_cavity_dyn_nml trim_ic_for_p_surf=.true. "// &
                           "requires enable=.true. (there is no ice load to "// &
                           "trim the initial column against)")
         has_error = .true.
      end if
      if (cfg%ocean%cavity_dyn%enable) then
         if (trim(cfg%sim_type) /= "ocean") then
            call logger%error("&ocean_cavity_dyn_nml enable=.true. requires "// &
                              "sim_type='ocean'")
            has_error = .true.
         end if
         ! --- geometry source envelope ---
         select case (trim(adjustl(cfg%ocean%cavity_dyn%draft_config)))
         case ("none", "flat", "linear")
            continue
         case ("file")
            ! Ships single-rank, through the PR-14 static-2-D reader
            ! (`ocean_data_input_load_static_2d`), which DOES apply the
            ! global offset — but the cavity as a whole is single-rank
            ! fenced below, and the loader re-asserts it.
            if (len_trim(cfg%ocean%cavity_dyn%draft_file) == 0) then
               call logger%error("&ocean_cavity_dyn_nml draft_config='file' requires "// &
                                 "draft_file")
               has_error = .true.
            end if
            if (len_trim(cfg%ocean%cavity_dyn%draft_var) == 0) then
               call logger%error("&ocean_cavity_dyn_nml draft_config='file' requires "// &
                                 "draft_var (the 2-D variable name; ISOMIP+ ships "// &
                                 "'iceDraft')")
               has_error = .true.
            end if
            if (parse_cavity_draft_sign(cfg%ocean%cavity_dyn%draft_sign) == &
                CAVITY_SIGN_INVALID) then
               call logger%error("&ocean_cavity_dyn_nml draft_sign='"// &
                                 trim(adjustl(cfg%ocean%cavity_dyn%draft_sign))// &
                                 "' is not recognised (depth|positive_down|"// &
                                 "elevation|positive_up).  There is no default that "// &
                                 "guesses from the data: the ISOMIP+ file carries an "// &
                                 "ELEVATION (z_d <= 0) and a depth file carries "// &
                                 "z_draft >= 0, and the two differ by the whole load.")
               has_error = .true.
            end if
         case default
            call logger%error("&ocean_cavity_dyn_nml draft_config='"// &
                              trim(adjustl(cfg%ocean%cavity_dyn%draft_config))// &
                              "' is not recognised (none|flat|linear|file)")
            has_error = .true.
         end select
         select case (trim(adjustl(cfg%ocean%cavity_dyn%draft_source)))
         case ("draft", "thickness")
            continue
         case ("in_situ")
            call logger%error("&ocean_cavity_dyn_nml draft_source='in_situ' (true "// &
                              "isostasy, p_ice = g*int(rho)) is not implemented: it "// &
                              "needs a per-column root find and does NOT admit exact "// &
                              "discrete rest in the split solver.  Use 'draft' "// &
                              "(the Boussinesq-isostatic flotation load ISOMIP+ "// &
                              "prescribes) or 'thickness'.")
            has_error = .true.
         case default
            call logger%error("&ocean_cavity_dyn_nml draft_source='"// &
                              trim(adjustl(cfg%ocean%cavity_dyn%draft_source))// &
                              "' is not recognised (draft|thickness|in_situ)")
            has_error = .true.
         end select
         ! `"linear"` measures its profile from `draft_x0`, so an
         ! unbounded (sentinel) anchor would make `draft_depth` meaningless
         ! and the whole shelf depth an artefact of 1e30*slope.
         if (trim(adjustl(cfg%ocean%cavity_dyn%draft_config)) == "linear" .and. &
             abs(cfg%ocean%cavity_dyn%draft_x0) >= 1.0e29_wp) then
            call logger%error("&ocean_cavity_dyn_nml draft_config='linear' requires "// &
                              "a finite draft_x0: it is the ANCHOR of the profile "// &
                              "(draft_depth is the draft AT draft_x0), not just the "// &
                              "western edge of the box.")
            has_error = .true.
         end if
         if (cfg%ocean%cavity_dyn%draft_depth < 0.0_wp) then
            call logger%error("&ocean_cavity_dyn_nml draft_depth must be >= 0 "// &
                              "(it is a DEPTH below z = 0, positive down)")
            has_error = .true.
         end if
         if (cfg%ocean%cavity_dyn%h_min_cavity <= 0.0_wp) then
            call logger%error("&ocean_cavity_dyn_nml h_min_cavity must be > 0 "// &
                              "(the grounding cutoff; 0 would admit a zero-thickness "// &
                              "water column under the ice)")
            has_error = .true.
         end if
         if (cfg%ocean%cavity_dyn%grounded_max_frac <= 0.0_wp .or. &
             cfg%ocean%cavity_dyn%grounded_max_frac > 1.0_wp) then
            call logger%error("&ocean_cavity_dyn_nml grounded_max_frac must be in "// &
                              "(0, 1] (the fraction of interior columns allowed to "// &
                              "ground)")
            has_error = .true.
         end if
         if (trim(adjustl(cfg%ocean%cavity_dyn%draft_source)) == "thickness" .and. &
             cfg%ocean%cavity_dyn%rho_ice <= 0.0_wp) then
            call logger%error("&ocean_cavity_dyn_nml draft_source='thickness' "// &
                              "requires rho_ice > 0")
            has_error = .true.
         end if
         ! MOM6 TRIM_IC_FOR_P_SURF.  The trim depth solves
         ! g*int_{-s}^{0} rho dz = p_ice_ref in CLOSED FORM, which needs a
         ! density that is affine in z above the ice base: the linear EOS
         ! over the analytic linear zinit profile.  A nonlinear EOS or a
         ! file profile would need a per-column root find against the
         ! column's own extrapolated T/S (MOM6 cut_off_column_top) and is
         ! not wired; a uniform_z seed lays interfaces from z = 0, not from
         ! the (trimmed) ice base.
         if (cfg%ocean%cavity_dyn%trim_ic_for_p_surf) then
            if (trim(adjustl(cfg%ocean%eos%eos)) /= "linear") then
               call logger%error("&ocean_cavity_dyn_nml trim_ic_for_p_surf=.true. "// &
                                 "requires &ocean_eos_nml eos='linear' (the trim "// &
                                 "depth is the closed-form root for a density "// &
                                 "affine in z; a nonlinear-EOS trim is not wired)")
               has_error = .true.
            end if
            if (.not. cfg%ocean%zinit%enable .or. &
                trim(adjustl(cfg%ocean%zinit%source)) /= "linear") then
               call logger%error("&ocean_cavity_dyn_nml trim_ic_for_p_surf=.true. "// &
                                 "requires &ocean_zinit_nml enable=.true., "// &
                                 "source='linear': the analytic T(z)/S(z) profile "// &
                                 "is what defines the density of the water the "// &
                                 "ice displaces")
               has_error = .true.
            end if
            if (trim(cfg%thickness_config) == "uniform_z") then
               call logger%error("&ocean_cavity_dyn_nml trim_ic_for_p_surf=.true. "// &
                                 "is incompatible with thickness_config='uniform_z'")
               has_error = .true.
            end if
         end if
         ! --- the one atmospheric-forcing path the cover mask does NOT
         !     reach (P2c) ---
         ! Every static forcing field is masked: the wind pair and the
         ! scalar q_heat/q_salt once at configure, the component bands
         ! every thermo step in the assembler.  The FILE-DRIVEN override
         ! is the exception: `ocean_data_forcing_apply` rewrites `tau_x`/
         ! `tau_y` (and, with `heat_to_component=.false.`, `Q_heat`
         ! itself) from the next time bracket with no access to
         ! `metrics%cover_frac` — it is handed `ss`, `sf`, `grid` and
         ! `bc`, and nothing else.  Re-masking per bracket means
         ! threading the metrics slot through the reader, which is a
         ! separate change.  Refused rather than half-wired: a cavity run
         ! whose wind is silently restored to its unmasked file value on
         ! the first bracket read looks entirely plausible and is wrong.
         if (cfg%ocean%dataovr%enable) then
            call logger%error("&ocean_cavity_dyn_nml enable=.true. is mutually "// &
                              "exclusive with &ocean_dataovr_nml enable=.true.  The "// &
                              "ice-cover mask on the atmospheric forcing is applied "// &
                              "to the wind pair at configure and to the surface-flux "// &
                              "components in the assembler; the data-override reader "// &
                              "rewrites tau_x/tau_y (and Q_heat, unless "// &
                              "heat_to_component=.true.) per time bracket without "// &
                              "the cover, which would restore the unmasked "// &
                              "atmosphere under the shelf.  Follow-up: thread "// &
                              "cover_frac through ocean_data_forcing_apply and the "// &
                              "ocean_seam_refresh_surface_stress seam.")
            has_error = .true.
         end if
         ! --- pressure-gradient envelope ---
         if (trim(adjustl(cfg%ocean%pgf%form)) /= "fv_mom6") then
            call logger%error("&ocean_cavity_dyn_nml enable=.true. requires "// &
                              "&ocean_pgf_nml form='fv_mom6' (got '"// &
                              trim(adjustl(cfg%ocean%pgf%form))//"'). Only the "// &
                              "FV_MOM6 family builds the pa(nz+1) pressure-stack "// &
                              "boundary condition the ice load is injected into; "// &
                              "mont hard-zeroes M(nz) and fv_lite/fv_wright seed "// &
                              "p_edge(nz+1)=0.")
            has_error = .true.
         end if
         ! P5.2 — the load must have a consumer once it has a GRADIENT.
         ! `p_top_in_bc` is the only route by which the isostatic load
         ! rho_ref*g*z_draft reaches the FV_MOM6 pa(nz+1) surface BC;
         ! without it a varying draft leaves the pressure stack ~5e6 Pa
         ! off its anomaly scale, the unsplit driver feels a raw
         ! g*grad(z_draft), and a non-uniform BT-correction weight turns the
         ! uncancelled depth-uniform force into a real per-layer shear.
         ! REFUSED rather than auto-enabled: an answer-changing knob that
         ! a second namelist group switches on behind the user's back is
         ! exactly the class of silent coupling this file exists to
         ! prevent.  A UNIFORM draft is exempt — a load with no gradient
         ! is bit-identically inert in the top BC (the theorem in
         ! `compute_fv_mom6_impl`'s docstring) — which is what keeps the
         ! flat-lid datum-equivalence gate expressible.  `draft_config`
         ! "none" is uniform (identically zero); "flat" is uniform only
         ! when no box bound clips it, else the calving front is a step.
         if (.not. cfg%ocean%pgf%p_top_in_bc) then
            block
               character(len=:), allocatable :: dcfg
               dcfg = trim(adjustl(cfg%ocean%cavity_dyn%draft_config))
               if (.not. cavity_draft_is_uniform(cfg)) then
                  call logger%error("&ocean_cavity_dyn_nml enable=.true. with "// &
                                    "draft_config='"//dcfg//"' requires "// &
                                    "&ocean_pgf_nml p_top_in_bc=.true.  That knob is "// &
                                    "the ONLY route by which the isostatic load "// &
                                    "rho_ref*g*z_draft reaches the FV_MOM6 pa(nz+1) "// &
                                    "surface boundary condition; without it a draft "// &
                                    "that VARIES leaves the pressure stack ~5e6 Pa "// &
                                    "off its anomaly scale and the column out of "// &
                                    "hydrostatic balance.  Only a draft that is "// &
                                    "uniform over the whole domain is exempt (a load "// &
                                    "with no gradient is provably inert there).")
                  has_error = .true.
               end if
            end block
         end if
         if (cfg%ocean%pgf%gfs_scale /= 1.0_wp) then
            call logger%error("&ocean_cavity_dyn_nml enable=.true. requires "// &
                              "&ocean_pgf_nml gfs_scale=1: the datum "// &
                              "(bt_H_ref, which the barotropic substep feels "// &
                              "through g_bt = gfs_scale*GRAVITY) and the load "// &
                              "(rho_ref*GRAVITY*z_draft, which the PGF feels "// &
                              "through GRAVITY) would then sit on two different "// &
                              "gravities and drift apart.")
            has_error = .true.
         end if
         ! --- vertical-coordinate envelope ---
         ! sigma (and zstar-lite, which shares its ocean branch) rescales
         ! the live column and so follows the draft for free; `z_fixed`
         ! is the FIRST family taught about the ice base explicitly
         ! (P6.2) — it reads `vcoord%z_top`, keeps its nominal interface
         ! depths GEOPOTENTIAL, vanishes the layers that outcrop into the
         ! ice to the inert filler and cuts the first live layer at the
         ! draft (Yung, Hallberg, Adcroft & Morrison 2026, JAMES 18,
         ! e2025MS005645, Fig. 1b).  Its own envelope is fenced below.
         !
         ! The refusal used to be a two-value whitelist whose message
         ! enumerated SIX families and named neither `lagrangian` nor
         ! `zstar_sigma` — both of which it refused.  A refusal that does
         ! not name what it refused, or gives a reason that is not the
         ! real one, sends the operator to fix the wrong thing.  So the
         ! accept test stays a whitelist (nothing else has been validated
         ! under a shelf) but the message now carries the offending
         ! family's OWN reason, and the reasons are not all "anchors at
         ! z = 0": three of the seven refused families are geometrically
         ! datum-safe and are refused for want of validation, which is a
         ! different, and recoverable, kind of no.  Measured per family in
         ! `test_ocean_vcoord_interface_depths`.
         block
            integer :: cav_vcoord_code
            real(wp) :: cav_h_nominal
            character(len=:), allocatable :: cav_reason
            cav_vcoord_code = parse_vcoord_type(cfg%vcoord_type, &
                                                default_code=VCOORD_EULERIAN_Z)
            if (.not. (cav_vcoord_code == VCOORD_SIGMA .or. &
                       cav_vcoord_code == VCOORD_Z_FIXED)) then
               select case (cav_vcoord_code)
               case (VCOORD_ZSTAR)
                  cav_reason = "'zstar' is MOM6 z*: the fixed z_fixed nominal "// &
                               "profile dilated by (H + eta)/H from the FREE "// &
                               "SURFACE.  It has no rigid-top branch (MOM6's "// &
                               "build_zstar_column z_rigid_top path is not "// &
                               "ported), so under a draft its fine near-surface "// &
                               "levels would hang from the ice base; it was "// &
                               "'sigma' under another name until the z* slice, "// &
                               "and is refused under a cavity until that branch "// &
                               "lands.  Use 'z_fixed' (the z-like family taught "// &
                               "the ice base) or 'sigma'"
               case (VCOORD_LAGRANGIAN)
                  cav_reason = "'lagrangian' is geometrically datum-FREE (the target "// &
                               "IS the live h_layer and the remap is a no-op), so the "// &
                               "placement objection does not apply to it.  It is "// &
                               "refused in v1 only because no cavity run has been "// &
                               "validated on an isopycnal coordinate; it is the "// &
                               "intended isopycnal control leg of the coordinate study"
               case (VCOORD_EULERIAN_Z)
                  cav_reason = "'eulerian_z' is a stretched SIGMA with the free "// &
                               "surface DROPPED (it is not a geopotential coordinate "// &
                               "despite the name), and the dropped eta — not a z "// &
                               "anchor — is why it cannot carry an ice-base "// &
                               "displacement"
               case (VCOORD_ZSIGMA)
                  cav_reason = "'zsigma' is refused on EVERY path, cavity or not: its "// &
                               "deep branch reads z_ref_global as metres while the "// &
                               "only writer fills it with the dimensionless k/nz"
               case (VCOORD_ZSTAR_SIGMA)
                  cav_reason = "'zstar_sigma' is a purely FRACTIONAL rescale of the "// &
                               "global reference table, hence datum-invariant and "// &
                               "geometrically safe under a draft.  It is refused in "// &
                               "v1 only because it follows BOTH boundaries (so it "// &
                               "solves nothing a cavity needs) and no cavity run has "// &
                               "been validated on it"
               case (VCOORD_ZSTAR_FULL)
                  cav_reason = "'zstar_full' builds its per-column reference table "// &
                               "from the TRUE bed while the target walk sees only the "// &
                               "live thickness, so under a draft it keeps the table's "// &
                               "SHALLOW entries: the fine near-surface band lands "// &
                               "against the ice base and the deep water is carried by "// &
                               "shallow-ocean spacing.  It does not merely anchor at "// &
                               "z = 0, it inverts which half of the column is resolved"
               case default
                  cav_reason = "'"//trim(cfg%vcoord_type)//"' is a DENSITY-space "// &
                               "coordinate with no geometric anchor, so the placement "// &
                               "objection does not apply.  It is refused in v1 because "// &
                               "cavity x isopycnal is unvalidated; note also that "// &
                               "hycom's z* nominal-floor band accumulates from the "// &
                               "column top, so under a shelf that band is "// &
                               "draft-FOLLOWING and must not be quoted as z-like"
               end select
               call logger%error("&ocean_cavity_dyn_nml enable=.true. accepts "// &
                                 "vcoord_type='sigma' or 'z_fixed' ONLY — the family "// &
                                 "that rescales the live column and so follows the "// &
                                 "ice base for free, plus the one that has been "// &
                                 "TAUGHT the ice base "// &
                                 "(z_fixed reads vcoord%z_top, keeps its nominal "// &
                                 "interface depths geopotential, and vanishes the "// &
                                 "layers that outcrop into the ice).  Got '"// &
                                 trim(cfg%vcoord_type)//"': "//cav_reason//".")
               has_error = .true.
            end if

            if (cav_vcoord_code == VCOORD_Z_FIXED) then
               ! ---- The staircase, and why this is a WARNING ----
               !
               ! A z-like coordinate removes the interior sigma tilt but
               ! replaces it with the ice-base STAIRCASE: where the draft
               ! crosses a nominal level the two columns' filler counts
               ! differ by one, the interface offset across that face
               ! jumps by up to `h_nominal`, and the FV_MOM6 acceleration
               ! in a vanished layer is h-INDEPENDENT.  The corrections
               ! that arrest it — top-side mass weighting (MWIPG) and the
               ! interior reference interface, Yung, Hallberg, Adcroft &
               ! Morrison (2026), JAMES 18, e2025MS005645, §3.3.2 and
               ! §3.2 — are NOT implemented.  A UNIFORM draft has no
               ! staircase (every column vanishes the same layers and
               ! cuts at the same depth, so every interface offset is
               ! identically zero) and is validated bit-zero to 30 days;
               ! a VARYING draft is not validated at all, and measured on
               ! `cavity_sloping_lid_rest_zfixed.nml` it is 47x the sigma
               ! leg's resting pressure-gradient residual at step 1 and
               ! ends in a non-finite state on day 18 (gfortran) or a
               ! saturated En = 3.8E-04 (nvfortran GPU).  That is the
               ! knob-OFF state.  `&vcoord_nml zfixed_closed_faces`
               ! (Adcroft, Hill & Marshall 1997 partial steps) closes the
               ! staircase faces, and with it the same file completes 30
               ! days and ISOMIP+ Ocean0 runs 30 days — but the residual on
               ! the faces left open is still ~2 decades above the sigma
               ! leg, so the warning stays, reworded for each state.
               !
               ! WARNING, not a refusal, deliberately: those corrections
               ! are the next slices and they need this configuration to
               ! be runnable to be developed and measured against.  What
               ! the user must not do is walk into it silently.
               if (.not. cavity_draft_is_uniform(cfg)) then
                  if (cfg%zfixed_closed_faces) then
                     call logger%warning("&vcoord_nml vcoord_type='z_fixed' under "// &
                                         "&ocean_cavity_dyn_nml with a draft that VARIES "// &
                                         "(draft_config='"// &
                                         trim(adjustl(cfg%ocean%cavity_dyn%draft_config))// &
                                         "') is EXPERIMENTAL.  &vcoord_nml "// &
                                         "zfixed_closed_faces=.true. closes the ice-base "// &
                                         "STAIRCASE faces, which is what makes this "// &
                                         "configuration runnable: the sloping-lid rest "// &
                                         "case completes 30 days and ISOMIP+ Ocean0 runs "// &
                                         "30 days.  It does not remove the staircase "// &
                                         "residual on the faces that stay open — the "// &
                                         "top-side mass weighting (MWIPG) and the interior "// &
                                         "reference interface, Yung, Hallberg, Adcroft & "// &
                                         "Morrison (2026), JAMES 18, e2025MS005645, "// &
                                         "sections 3.3.2 and 3.2, are not implemented — so "// &
                                         "the resting sloping-lid case still carries about "// &
                                         "two decades more spurious energy than its sigma "// &
                                         "leg.  Only a UNIFORM draft is bit-zero at rest.")
                  else
                     call logger%warning("&vcoord_nml vcoord_type='z_fixed' under "// &
                                         "&ocean_cavity_dyn_nml with a draft that VARIES "// &
                                         "(draft_config='"// &
                                         trim(adjustl(cfg%ocean%cavity_dyn%draft_config))// &
                                         "') and &vcoord_nml zfixed_closed_faces=.false. "// &
                                         "is NOT VALIDATED.  Only a UNIFORM draft is: "// &
                                         "there every column vanishes the same layers and "// &
                                         "cuts at the same depth, so the answer is "// &
                                         "bit-zero at rest.  Where the draft crosses a "// &
                                         "nominal level the filler count changes column "// &
                                         "to column and the FV pressure gradient across "// &
                                         "the open ice-base STAIRCASE drives a spurious "// &
                                         "flow: measured at rest, 47x the sigma leg's "// &
                                         "step-1 residual, and the run does not survive "// &
                                         "30 days.  Set zfixed_closed_faces=.true. (the "// &
                                         "partial-step face closure, with which a varying "// &
                                         "draft does survive 30 days), or use "// &
                                         "vcoord_type='sigma' for a sloping lid.")
                  end if
               end if
               ! ---- z_fixed × cavity, v1 envelope ----
               !
               ! Under a quasi-geopotential coordinate the ice base cuts
               ! the nominal stack, so on an ICE-COVERED column `k = nz`
               ! is an inert filler (`h <= H_VANISHED`), NOT the
               ! ice-adjacent live layer.  This is a state no consumer in
               ! the tree has ever seen: no family on this branch
               ! vanishes a layer against the TOP.  The shared
               ! `k_top(i,j)` (the first live layer, counting down) that
               ! fixes them is the NEXT slice — P6.3 for the tracer/flux
               ! consumers, P6.4 for momentum and the boundary-layer
               ! schemes — so until it lands the only thing this
               ! combination may run is ADIABATIC DYNAMICS.
               !
               ! What is NOT refused, and why: OPEN-OCEAN columns have
               ! `z_top = 0`, so `k = nz` is live there and behaves
               ! exactly as today; and on a COVERED column the binary
               ! cover mask (`cover_frac`) already zeroes every
               ! ATMOSPHERIC input at source — wind stress (and with it
               ! `stress_mag`, hence u*), the scalar and assembled
               ! surface heat/salt fluxes, the shortwave deposit and both
               ! restoring increments.  Those therefore compose with a
               ! vanished `k = nz` and are left alone.  What follows is
               ! everything that acts ON the ice-adjacent layer itself,
               ! and so is not masked by anything.
               ! NOTE `&ocean_cavity_melt_nml enable` and
               ! `&ocean_tdrag_nml enable` used to be refused HERE, and
               ! are not any more: both are routed through the shared
               ! first-live-layer index `ms%k_top` (and its two face
               ! twins) — P6.3/P6.4.  The melt heat/salt deposit and the
               ! `freshwater="mass"` column source land on
               ! `k_top(i,j)`; the ice-ocean drag's band walk starts at
               ! `k_top_u/v`, gates on `H_VANISHED` instead of on zero,
               ! and captures its implicit-fold rate on the same row the
               ! vdiff diagonal adds it to.  `k_top ≡ nz` off a rigid
               ! top, so nothing else moved.
               if (cfg%ocean%vmix%use_kpp) then
                  call logger%error("&ocean_vmix_nml use_kpp=.true. is refused with "// &
                                    "vcoord_type='z_fixed' under a cavity: KPP's "// &
                                    "surface reference column is k = nz "// &
                                    "(b_ref = -g*rho_layer(nz)/rho_0, "// &
                                    "d_centre_ref = h(nz)/2), which on a covered "// &
                                    "column is the filler — a spurious buoyancy jump "// &
                                    "at the very first interface and a reference "// &
                                    "depth of ~1e-4 m.  Set use_kpp=.false. (an "// &
                                    "adiabatic cavity run is the v1 envelope).  "// &
                                    "Needs the shared k_top: follow-up P6.4.")
                  has_error = .true.
               end if
               if (cfg%ocean%epbl%enable) then
                  call logger%error("&ocean_epbl_nml enable=.true. is refused with "// &
                                    "vcoord_type='z_fixed' under a cavity: the EPBL "// &
                                    "column captures the surface at k = nz and gates "// &
                                    "on `h > 0` rather than on H_VANISHED, so a "// &
                                    "filler top layer reaches the specific-volume "// &
                                    "derivatives as an absurd concentration.  Needs "// &
                                    "the shared k_top: follow-up P6.4.")
                  has_error = .true.
               end if
               if (cfg%ocean%tracers%enable_ideal_age) then
                  call logger%error("&ocean_tracers_nml enable_ideal_age=.true. is "// &
                                    "refused with vcoord_type='z_fixed' under a "// &
                                    "cavity: the young-band reset writes "// &
                                    "hTr_age(:,:,nz) = young*h(:,:,nz), which on a "// &
                                    "covered column ventilates a filler (and there "// &
                                    "is no ventilation under a shelf at all).  "// &
                                    "Follow-up P6.3.")
                  has_error = .true.
               end if
               if (cfg%ocean%gm%enable) then
                  call logger%error("&ocean_gm_nml enable=.true. is refused with "// &
                                    "vcoord_type='z_fixed' under a cavity: without "// &
                                    "zfixed_closed_faces the non-divergence closure "// &
                                    "dumps the residual streamfunction transport "// &
                                    "into k = nz (a filler under the ice); with it "// &
                                    "the closure is open-column, but the slopes "// &
                                    "slot GM needs is itself refused under a "// &
                                    "cavity (below).  GM x cavity is unvalidated "// &
                                    "on any coordinate; revisited with the "// &
                                    "coordinate study.")
                  has_error = .true.
               end if
               if (cfg%ocean%redi%enable .or. cfg%ocean%slopes%enable) then
                  call logger%error("&ocean_redi_nml / &ocean_slopes_nml enable="// &
                                    ".true. is refused with vcoord_type='z_fixed' "// &
                                    "under a cavity: the isopycnal-slope surface "// &
                                    "fill is built from h(:,:,nz), the filler on a "// &
                                    "covered column.  Unvalidated with a cavity on "// &
                                    "any coordinate; revisited with the coordinate "// &
                                    "study.")
                  has_error = .true.
               end if
               ! ---- kappa_h: refused ONLY with the face mask off ----
               !
               ! The objection is unchanged where it still applies: the
               ! along-coordinate tracer diffusion gates its T = hTr/h
               ! division on `h > 0` (1/0 armour) and not on H_VANISHED,
               ! weights the face flux by the ARITHMETIC mean thickness
               ! — a filler beside a 20 m cell is weighted by 10 m — and
               ! carries no mass-availability limiter, so it can drive a
               ! vanished cell's hTr strongly negative in one step.
               !
               ! But `&vcoord_nml zfixed_closed_faces` already answers
               ! it, from the other end: `ocean_hdiff_tracer_step`
               ! multiplies every face flux by `metrics%open_u/open_v`,
               ! which is exactly zero wherever the layer is a filler on
               ! EITHER side.  A filler cell then has all four of its
               ! own-layer faces closed, so its hTr divergence is
               ! identically zero and the garbage concentration the
               ! `h > 0` gate computes for it never leaves the cell.
               ! Flux-zero, not flux-limited, so the scheme stays
               ! conservative by construction.  That is the P6.5 gate,
               ! reached through the mask instead of through a new
               ! threshold — and it is a strictly stronger statement,
               ! because it also closes the partial⇄filler face the
               ! threshold alone would leave open on the thick side.
               if (cfg%ocean%hdiff%kappa_h /= 0.0_wp .and. &
                   .not. cfg%zfixed_closed_faces) then
                  call logger%error("&ocean_hdiff_nml kappa_h /= 0 is refused with "// &
                                    "vcoord_type='z_fixed' under a cavity UNLESS "// &
                                    "&vcoord_nml zfixed_closed_faces=.true.: the "// &
                                    "along-coordinate tracer diffusion gates its "// &
                                    "T = hTr/h division on `h > 0` (1/0 armour), "// &
                                    "not on H_VANISHED, weights the face flux by the "// &
                                    "ARITHMETIC mean thickness — so a filler beside "// &
                                    "a 20 m cell is weighted by 10 m — and has no "// &
                                    "mass-availability limiter, so it can drive a "// &
                                    "vanished cell's hTr strongly negative in one "// &
                                    "step.  With the partial-step face mask on, "// &
                                    "every face touching a filler is CLOSED and the "// &
                                    "flux is exactly zero, which answers all three.  "// &
                                    "Set zfixed_closed_faces=.true., or kappa_h=0.")
                  has_error = .true.
               end if
               if (cfg%ocean%kshear%enable) then
                  call logger%error("&ocean_kappa_shear_nml enable=.true. is refused "// &
                                    "with vcoord_type='z_fixed' under a cavity: the "// &
                                    "JHL08 column solve closes its SURFACE row on "// &
                                    "k = nz (u_c/v_c/t_c/s_c(nz)), which on a "// &
                                    "covered column is an inert filler inside the "// &
                                    "ice draft, and its own massless-merge helper is "// &
                                    "off by default.  Not covered by the shared "// &
                                    "k_top (P6.3/P6.4), which routes the FORCING "// &
                                    "sites; a column solver wants the compacted "// &
                                    "column rdb_massless already builds.")
                  has_error = .true.
               end if
               if (cfg%ocean%tidal_mixing%enable) then
                  call logger%error("&ocean_tidal_mixing_nml enable=.true. is "// &
                                    "refused with vcoord_type='z_fixed' under a "// &
                                    "cavity: the N^2 column sets its top boundary "// &
                                    "at k = nz and forms the k = nz-1 interface "// &
                                    "spacing as 0.5*(h(nz-1) + h(nz)), which on a "// &
                                    "covered column HALVES that spacing against a "// &
                                    "filler and inflates the buoyancy frequency at "// &
                                    "the first live interface.  Unvalidated under a "// &
                                    "rigid top.")
                  has_error = .true.
               end if
               if (cfg%regrid_time_scale > 0.0_wp) then
                  call logger%error("&vcoord_nml regrid_time_scale > 0 is refused "// &
                                    "with vcoord_type='z_fixed' under a cavity: the "// &
                                    "grid time-filter is a per-layer convex blend "// &
                                    "that does not respect the vanish marker, so a "// &
                                    "filler relaxing toward h_min from a live "// &
                                    "thickness passes THROUGH H_VANISHED and the "// &
                                    "layer oscillates live/dead on successive steps "// &
                                    "— the remap drain deleting its content on the "// &
                                    "step it reads dead.  Excluding the fillers from "// &
                                    "the blend is its own change.")
                  has_error = .true.
               end if
               ! ---- In-layer T/S reconstruction for the PGF: REFUSED ----
               !
               ! Measured on ISOMIP+ Ocean0 idealised (z_fixed + closed
               ! faces, melt off), V100, on the v0.1.0 defaults (bebt =
               ! 0.1, renorm_consistent_flux, I1'): from rest the knob
               ! spins up En = 3.4E-04 m2/s2 in the first 3 hours
               ! (MaxCFL 0.43) and holds ~1E-03 (MaxCFL up to 0.85) for
               ! 30 days — ~19 000x the layer-mean-density run's 5.6E-08
               ! and ~5x the melt-ON circulation.  (Before I1' and bebt =
               ! 0.1 it went non-finite at outer step 118, day 0.41.)  The
               ! PLM/PPM edge build reads the inert top-side FILLERS as
               ! neighbouring water at the partial top cell.  The
               ! filler-aware reconstruction that skips them is a held
               ! slice, so until it lands this is a refusal, not a
               ! warning: the spurious flow is there within 3 hours.
               ! (`cavity_rest_growth_diagnosis.md` §Q.0 item 9.)
               if (cfg%ocean%pgf%reconstruct_for_pressure) then
                  call logger%error("&ocean_pgf_nml reconstruct_for_pressure=.true. "// &
                                    "is refused with vcoord_type='z_fixed' under a "// &
                                    "cavity: the in-layer PLM/PPM T/S edge build "// &
                                    "reads the layer means of the inert top-side "// &
                                    "FILLERS as neighbouring water at the partial top "// &
                                    "cell — ISOMIP+ Ocean0 (melt off, from rest) spins "// &
                                    "up a spurious En ~ 1E-03 m2/s2 within hours, "// &
                                    "19 000x the layer-mean-density run.  Pending the "// &
                                    "filler-aware reconstruction that skips vanished "// &
                                    "layers; set reconstruct_for_pressure=.false. "// &
                                    "(the layer-mean PCM density) until it lands.")
                  has_error = .true.
               end if
               ! ---- Lateral viscosity lower envelope: a WARNING ----
               !
               ! Not a refusal: the vcoord stability matrix runs this
               ! combination inviscid on purpose, to measure the mode.
               ! But a user who sets nu_h below the envelope should be
               ! told.  See `zfixed_cavity_nu_h_below_envelope`.
               if (zfixed_cavity_nu_h_below_envelope(cfg)) then
                  call logger%warning("&ocean_hvisc_nml nu_h = "// &
                                      to_string(cfg%ocean%hvisc%nu_h)// &
                                      " m2/s is below the "// &
                                      to_string(ZFIXED_CAVITY_NU_H_MIN)// &
                                      " m2/s lower envelope of vcoord_type='z_fixed' "// &
                                      "under a cavity.  Measured on ISOMIP+ Ocean0 "// &
                                      "(2 km, melt off): nu_h = 0 carries an INVISCID "// &
                                      "mode that grows EXPONENTIALLY at 0.18 /day "// &
                                      "(5.6-day e-folding, accelerating; En 1.4E-06 at "// &
                                      "day 30, 26x the protocol leg) while nu_h = 2 "// &
                                      "already decelerates.  Flow-aware closures do "// &
                                      "not replace it at these speeds (Smagorinsky "// &
                                      "adds ~1 m2/s).  The ISOMIP+ Table-4 value is "// &
                                      "6; nu_h >= 30 also removes the calving-front "// &
                                      "partial-cell jet (cavity_rest_growth_diagnosis "// &
                                      "section Q).  Proceeding, as configured.")
               end if
               ! ISOMIP+ (Asay-Davis et al. 2016 §3.1.5): "the minimum
               ! thickness is likely to be approximately two grid cells
               ! (~40 m if z levels are equally spaced)".  Under a
               ! terrain-following coordinate a just-afloat column still
               ! carries all nz layers; under a z-like one a column
               ! thinner than 2*h_nominal carries ONE partial live layer
               ! and nz-1 fillers, and every column-walking consumer then
               ! operates on a single cell.  `h_nominal` is
               ! `max_depth/nz` — Z_FIXED's spacing is written from
               ! `&ocean_topo_nml max_depth`, deliberately with no second
               ! spelling.
               cav_h_nominal = 0.0_wp
               if (cfg%nz_layers > 0) then
                  cav_h_nominal = cfg%ocean%topo%max_depth/real(cfg%nz_layers, wp)
               end if
               ! A stretched profile has no single `h_nominal`: whether a
               ! thin cavity column spans two layers depends on the DEPTH
               ! of its draft, which configure does not see.  The check
               ! is then made against the thickest nominal layer and
               ! downgraded to a warning — conservative, and it never
               ! refuses a configuration the geometry might satisfy.
               if (parse_z_fixed_profile(cfg%z_fixed_profile) /= ZFIXED_PROFILE_UNIFORM .and. &
                   parse_z_fixed_profile(cfg%z_fixed_profile) /= ZFIXED_PROFILE_INVALID .and. &
                   cfg%nz_layers > 0) then
                  block
                     real(wp), allocatable :: cav_dz(:)
                     integer :: cav_ierr
                     allocate (cav_dz(cfg%nz_layers))
                     call z_fixed_nominal_dz(parse_z_fixed_profile(cfg%z_fixed_profile), &
                                             cfg%nz_layers, cfg%ocean%topo%max_depth, &
                                             cfg%z_fixed_dz, cfg%z_fixed_dz_top, &
                                             cfg%z_fixed_tanh_center, &
                                             cfg%z_fixed_tanh_width, cav_dz, cav_ierr)
                     if (cav_ierr == ZFIXED_DZ_OK .and. &
                         cfg%ocean%cavity_dyn%h_min_cavity < 2.0_wp*maxval(cav_dz)) then
                        call logger%warning("&ocean_cavity_dyn_nml h_min_cavity = "// &
                                            to_string(cfg%ocean%cavity_dyn%h_min_cavity)// &
                                            " m is below twice the thickest nominal "// &
                                            "z_fixed layer ("//to_string(maxval(cav_dz))// &
                                            " m) of the stretched profile: a cavity column "// &
                                            "thinner than two nominal layers AT ITS DEPTH "// &
                                            "carries a single partial live layer (ISOMIP+ "// &
                                            "Asay-Davis et al. 2016, §3.1.5).  Not refused: "// &
                                            "under a stretched profile it depends on the draft.")
                     end if
                  end block
                  cav_h_nominal = 0.0_wp
               end if
               if (cav_h_nominal > 0.0_wp .and. &
                   cfg%ocean%cavity_dyn%h_min_cavity < 2.0_wp*cav_h_nominal) then
                  call logger%error("&ocean_cavity_dyn_nml h_min_cavity = "// &
                                    to_string(cfg%ocean%cavity_dyn%h_min_cavity)// &
                                    " m is below 2*h_nominal = "// &
                                    to_string(2.0_wp*cav_h_nominal)//" m "// &
                                    "(h_nominal = &ocean_topo_nml max_depth / "// &
                                    "nz_layers = "//to_string(cav_h_nominal)// &
                                    " m), which vcoord_type='z_fixed' requires under "// &
                                    "a cavity: a thinner water column carries a "// &
                                    "single partial live layer and nz-1 inert "// &
                                    "fillers.  This is ISOMIP+'s own rule "// &
                                    "(Asay-Davis et al. 2016, GMD 9, 2471, "// &
                                    "§3.1.5).  NOTE it moves the grounding line "// &
                                    "relative to a sigma leg — a confound the "// &
                                    "coordinate study must control for.")
                  has_error = .true.
               end if
            end if
         end block
         if (trim(cfg%thickness_config) == "uniform_z") then
            call logger%error("&ocean_cavity_dyn_nml enable=.true. is mutually "// &
                              "exclusive with thickness_config='uniform_z': that "// &
                              "seed lays uniform z interfaces from z = 0 down, so "// &
                              "under a draft it would fill the ice with water.  Use "// &
                              "the default sigma-style split.")
            has_error = .true.
         end if
         ! NOTE: `&ocean_zinit_nml enable` used to be refused here — the
         ! overlay measured depth from the COLUMN TOP, which under a
         ! draft is `z_draft` metres below `z = 0`, so a geopotential
         ! profile landed systematically too shallow.  `build_z_ctr` now
         ! takes the column-top depth and the seed passes
         ! `metrics%z_draft`, so the two compose and the refusal is gone.
         ! --- solver envelope ---
         if (cfg%ocean%bt%n_inner < 1 .and. .not. cfg%ocean%bt%auto_n_inner) then
            call logger%error("&ocean_cavity_dyn_nml enable=.true. requires the "// &
                              "SPLIT solver (&ocean_bt_nml n_inner >= 1, or "// &
                              "auto_n_inner): the unsplit driver has neither the "// &
                              "barotropic correction nor the eta_forcing seam, so it "// &
                              "carries no barotropic response to a surface load and "// &
                              "the cavity is unvalidated there.")
            has_error = .true.
         end if
         if (cfg%ocean%bt%bt_halo > 0) then
            call logger%error("&ocean_cavity_dyn_nml enable=.true. is mutually "// &
                              "exclusive with &ocean_bt_nml bt_halo > 0: the "// &
                              "wide-halo BT clone rebuilds its own metrics from the "// &
                              "grid formula and carries no z_draft, so its reference "// &
                              "depth would be the bed and its solve would ignore the "// &
                              "ice.")
            has_error = .true.
         end if
         if (cfg%px*cfg%py > 1) then
            call logger%error("&ocean_cavity_dyn_nml enable=.true. is single-rank in "// &
                              "v1 (px*py = "//to_string(cfg%px*cfg%py)//").  The "// &
                              "draft halo itself is two lines, but the grounding "// &
                              "statistics are global reductions the v1 configure does "// &
                              "not take, and draft_config='file' inherits the "// &
                              "local-nx reader.  Lifting the fence is its own PR.")
            has_error = .true.
         end if
         ! --- mutually exclusive capabilities ---
         if (cfg%ocean%wetdry%enable) then
            call logger%error("&ocean_cavity_dyn_nml enable=.true. is mutually "// &
                              "exclusive with &ocean_wetdry_nml enable: both decide "// &
                              "whether a column can carry water, from different "// &
                              "thresholds, and wet/dry also forces split_scheme="// &
                              "'ssp_rk2'.")
            has_error = .true.
         end if
         if (cfg%ocean%porous%enable) then
            call logger%error("&ocean_cavity_dyn_nml enable=.true. is mutually "// &
                              "exclusive with &ocean_porous_nml enable: both narrow "// &
                              "the same faces from static geometry and the "// &
                              "combination is unvalidated.")
            has_error = .true.
         end if
         if (cfg%ocean%ice%enable) then
            call logger%error("&ocean_cavity_dyn_nml enable=.true. is mutually "// &
                              "exclusive with &ocean_ice_nml enable: sea ice is a "// &
                              "SECOND surface load on the same column, and the two "// &
                              "are not reconciled in v1.")
            has_error = .true.
         end if
         if (cfg%ocean%tides%enable .and. cfg%ocean%tides%use_sal) then
            call logger%error("&ocean_cavity_dyn_nml enable=.true. is mutually "// &
                              "exclusive with &ocean_tides_nml use_sal: the scalar "// &
                              "SAL elevation is beta_sal*bt_eta, and under the cavity "// &
                              "datum bt_eta is the departure from the LOADED "// &
                              "equilibrium, not the sea-surface elevation the SAL "// &
                              "response is defined on.  The body tide itself "// &
                              "(enable alone) is datum-independent and allowed.")
            has_error = .true.
         end if
         ! --- honest-inertness warnings (the house rule: warn, do not refuse) ---
         if (trim(adjustl(cfg%ocean%cavity_dyn%draft_config)) == "none") then
            call logger%warning("&ocean_cavity_dyn_nml enable=.true. with "// &
                                "draft_config='none': z_draft is identically zero, so "// &
                                "bt_H_ref = b and the run is bit-identical to a "// &
                                "cavity-free one.")
         end if
         ! Design Q4: WARN, do not refuse, on cavity x in_eos=.false.  The
         ! load is depth-uniform in `pa`, so the rest and equivalence
         ! gates pass either way; what is wrong without `in_eos` is the
         ! THERMOBARICITY, and only a pressure-dependent EOS has any.  A
         ! linear EOS is pressure-blind, so there the knob is honestly
         ! inert and there is nothing to warn about.
         if (.not. cfg%ocean%psurf%in_eos .and. &
             trim(adjustl(cfg%ocean%eos%eos)) /= "linear") then
            call logger%warning("&ocean_cavity_dyn_nml enable=.true. with a "// &
                                "NONLINEAR equation of state (&ocean_eos_nml eos='"// &
                                trim(adjustl(cfg%ocean%eos%eos))//"') but without "// &
                                "&ocean_psurf_nml in_eos=.true.: the in-situ EOS "// &
                                "pressure still starts at 0 Pa at the ice base, so "// &
                                "the EOS ignores up to ~5e6 Pa of ice load (wrong "// &
                                "thermobaricity, wrong freezing point).  Harmless "// &
                                "for the rest and equivalence gates; not for a "// &
                                "production cavity.")
         end if
      end if
      ! ---- Ice-shelf basal melt, v1 scope (Phase 2b) ----
      ! Every restriction fails loud.  One of them exists because a piece
      ! of the coupling is NOT in this slice: there is no per-cell cover
      ! mask on the atmospheric forcing yet, and leaving that silently
      ! half-wired would run an atmosphere through several hundred metres
      ! of solid ice.  The interface pressure is NOT such a gap — the
      ! cavity assembles `ms%p_top = p_ice_ref + sf%p_surf` (P5.2) and
      ! the melt liquidus is its third consumer.
      if (cfg%ocean%cavity_melt%enable) then
         if (.not. cfg%ocean%cavity_dyn%enable) then
            call logger%error("&ocean_cavity_melt_nml enable=.true. requires "// &
                              "&ocean_cavity_dyn_nml enable=.true.  The melt "// &
                              "interface stands on that group's geometry: without a "// &
                              "draft there is no ice base, cover_frac is identically "// &
                              "zero and the kernel would never be called.")
            has_error = .true.
         end if
         if (trim(adjustl(cfg%ocean%eos%tfreeze_set)) /= "isomip") then
            call logger%error("&ocean_cavity_melt_nml enable=.true. requires "// &
                              "&ocean_eos_nml tfreeze_set='isomip' (got '"// &
                              trim(adjustl(cfg%ocean%eos%tfreeze_set))//"').  The "// &
                              "two shipped liquidi differ by ~0.03 degC at S = 34.5, "// &
                              "which is enough to flip the SIGN of the basal melt "// &
                              "rate over a 0.03 degC band of far-field temperature — "// &
                              "the sea-ice set is not a defensible default for a "// &
                              "cavity.")
            has_error = .true.
         end if
         if (.not. cfg%ocean%forcing%enable_components) then
            call logger%error("&ocean_cavity_melt_nml enable=.true. requires "// &
                              "&ocean_forcing_nml enable_components=.true.  The melt "// &
                              "fluxes are delivered as the OWNED components "// &
                              "heat_cavity/salt_cavity, which are allocated only "// &
                              "with the component set, and only "// &
                              "ocean_surface_flux_assemble folds them into "// &
                              "Q_heat/Q_salt.  Writing Q_heat/Q_salt directly is "// &
                              "refused by the fill contract — the sea-ice coupler "// &
                              "full-overwrites those.")
            has_error = .true.
         end if
         block
            integer :: melt_law_code, melt_ice_code
            melt_law_code = parse_cavity_exchange_law(cfg%ocean%cavity_melt%exchange_law)
            select case (melt_law_code)
            case (CAVITY_LAW_CONST_GAMMA, CAVITY_LAW_HJ99, CAVITY_LAW_YUNG25)
               continue
            case (CAVITY_LAW_INVALID)
               call logger%error("&ocean_cavity_melt_nml exchange_law = '"// &
                                 trim(adjustl(cfg%ocean%cavity_melt%exchange_law))// &
                                 "' is not recognised (shipped: const_gamma, hj99, "// &
                                 "yung25).")
               has_error = .true.
            case default
               call logger%error("&ocean_cavity_melt_nml exchange_law = '"// &
                                 trim(adjustl(cfg%ocean%cavity_melt%exchange_law))// &
                                 "' is RESERVED, not implemented "// &
                                 "(CAVITY_MELT_NOT_IMPLEMENTED).  Its enum value is "// &
                                 "nailed down so adding it later is not a "// &
                                 "renumbering, and the Python prototype has it, but "// &
                                 "no Fortran physics ships.  Shipped: const_gamma, "// &
                                 "hj99, yung25.")
               has_error = .true.
            end select
            ! Holland & Jenkins (1999) eq. (15) p. 1792 takes
            ! `ln(u* xi_N eta*^2 / (|f| h_nu))` and eq. (18) divides by
            ! `f L_O`: the law does not exist on the equator.  The
            ! per-column check over the covered cells is taken at
            ! configure (`configure_ocean_cavity_melt`), where f_centre
            ! exists; here we can only catch the whole-domain f = 0 case,
            ! and catching it early is worth the duplication.
            if (melt_law_code == CAVITY_LAW_HJ99) then
               if (cfg%coriolis_f == 0.0_wp .and. &
                   cfg%ocean%topo%coriolis_beta == 0.0_wp) then
                  call logger%error("&ocean_cavity_melt_nml exchange_law='hj99' on an "// &
                                    "f = 0 grid (coriolis_f = 0, coriolis_beta = 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 — the same "// &
                                    "fail-loud stance &ocean_vmix_nml bkgnd_henyey "// &
                                    "takes on a cartesian grid.  Use "// &
                                    "exchange_law='const_gamma' or set a Coriolis "// &
                                    "parameter.")
                  has_error = .true.
               end if
            end if
            melt_ice_code = parse_cavity_ice_mode(cfg%ocean%cavity_melt%ice_conduction)
            select case (melt_ice_code)
            case (CAVITY_ICE_INSULATING, CAVITY_ICE_ADV_DIFF)
               continue
            case (CAVITY_ICE_INVALID)
               call logger%error("&ocean_cavity_melt_nml ice_conduction = '"// &
                                 trim(adjustl(cfg%ocean%cavity_melt%ice_conduction))// &
                                 "' is not recognised (shipped: insulating, "// &
                                 "adv_diff).")
               has_error = .true.
            case default
               call logger%error("&ocean_cavity_melt_nml ice_conduction = '"// &
                                 trim(adjustl(cfg%ocean%cavity_melt%ice_conduction))// &
                                 "' is RESERVED, not implemented "// &
                                 "(CAVITY_MELT_NOT_IMPLEMENTED).  The steady "// &
                                 "diffusive form makes q_ice independent of m_mass, "// &
                                 "so sign(m) = sign(T*) no longer holds and the "// &
                                 "pre-solve melt/freeze branch has to be revisited — "// &
                                 "it is not a coefficient change.")
               has_error = .true.
            end select
         end block
         if (cfg%ocean%cavity_melt%gamma_t <= 0.0_wp) then
            call logger%error("&ocean_cavity_melt_nml gamma_t must be > 0 (it "// &
                              "multiplies u* to give the heat exchange velocity; "// &
                              "zero is identically zero melt, which is what "// &
                              "enable=.false. is for).")
            has_error = .true.
         end if
         if (cfg%ocean%cavity_melt%gamma_s >= 0.0_wp .and. &
             cfg%ocean%cavity_melt%gamma_s <= 0.0_wp) then
            call logger%error("&ocean_cavity_melt_nml gamma_s = 0 is refused: the "// &
                              "three-equation form divides by gamma_s.  Leave it "// &
                              "NEGATIVE (the default) to take the ISOMIP+ "// &
                              "gamma_t/35, or set a positive value.")
            has_error = .true.
         end if
         if (cfg%ocean%cavity_melt%far_field_depth <= 0.0_wp) then
            call logger%error("&ocean_cavity_melt_nml far_field_depth must be > 0 m "// &
                              "(it is the thickness the far-field T/S/u are averaged "// &
                              "over; zero would sample nothing).")
            has_error = .true.
         end if
         ! --- Phase 3: real freshwater MASS, and its sea-level partner ---
         ! `freshwater="mass"` moves the meltwater as a REAL Boussinesq
         ! volume on the top layer.  Two envelope holes are refused BY
         ! NAME rather than half-wired, because each would put the mass
         ! source and the machinery that owns `h_layer(:,:,nz)` out of
         ! step with one another:
         !
         !   * dynamic wet/dry re-decides every step which columns carry
         !     water, and its positive-definite outflow limiter is the
         !     other writer of a top-layer thickness source.  Composing
         !     the two needs the limiter to SEE the melt volume;
         !   * a windowed tracer-advection ratio > 1 freezes `hTr` for
         !     `ratio` steps while `h` keeps moving, so the dilution the
         !     mass form relies on would be applied to a tracer load that
         !     is deliberately stale.
         block
            integer :: fw_code, vc_code
            fw_code = parse_cavity_freshwater(cfg%ocean%cavity_melt%freshwater)
            vc_code = parse_cavity_volume_comp(cfg%ocean%cavity_melt%volume_compensation)
            if (fw_code == CAVITY_FW_INVALID) then
               call logger%error("&ocean_cavity_melt_nml freshwater = '"// &
                                 trim(adjustl(cfg%ocean%cavity_melt%freshwater))// &
                                 "' is not recognised (shipped: virtual, mass).")
               has_error = .true.
            end if
            if (vc_code == CAVITY_VC_INVALID) then
               call logger%error("&ocean_cavity_melt_nml volume_compensation = '"// &
                                 trim(adjustl(cfg%ocean%cavity_melt%volume_compensation))// &
                                 "' is not recognised (shipped: none, "// &
                                 "uniform_open_ocean).")
               has_error = .true.
            end if
            if (fw_code == CAVITY_FW_MASS) then
               if (cfg%ocean%wetdry%enable) then
                  call logger%error("&ocean_cavity_melt_nml freshwater='mass' is "// &
                                    "refused with &ocean_wetdry_nml enable=.true.  "// &
                                    "Wet/dry owns the other top-layer thickness "// &
                                    "source (its positive-definite outflow limiter) "// &
                                    "and re-decides per step which columns hold "// &
                                    "water; composing the two needs the limiter to "// &
                                    "see the melt volume.  Follow-up: "// &
                                    "'cavity real freshwater under wet/dry'.")
                  has_error = .true.
               end if
               if (cfg%ocean%vmix%dt_tracer_advect_ratio > 1) then
                  call logger%error("&ocean_cavity_melt_nml freshwater='mass' is "// &
                                    "refused with &ocean_vmix_nml "// &
                                    "dt_tracer_advect_ratio > 1.  The windowed drain "// &
                                    "holds hTr fixed for the window while h keeps "// &
                                    "moving, so the dilution the mass form relies on "// &
                                    "would act on a deliberately stale tracer load.  "// &
                                    "Follow-up: 'cavity real freshwater under the "// &
                                    "windowed tracer-advect drain'.")
                  has_error = .true.
               end if
            end if
            if (vc_code == CAVITY_VC_UNIFORM_OPEN .and. fw_code /= CAVITY_FW_MASS) then
               call logger%error("&ocean_cavity_melt_nml volume_compensation="// &
                                 "'uniform_open_ocean' requires freshwater='mass'.  "// &
                                 "The virtual form adds no volume, so there is "// &
                                 "nothing to compensate and the sink would be a "// &
                                 "pure, unexplained mass loss.")
               has_error = .true.
            end if
         end block
         ! --- the cover mask SHIPS (P2c) ---
         ! Wind stress, surface restoring, shortwave penetration and the
         ! uniform scalar q_heat/q_salt were each refused here while
         ! there was no per-cell ice-COVER MASK on the atmospheric
         ! forcing.  There is one now:
         !
         !   * the wind-stress PAIR is masked face-wise at configure
         !     (`ocean_surface_stress_apply_cover`, called from
         !     `configure_ocean_cavity`), which also silences the
         !     implicit vdiff stress fold and the MLE front sampler —
         !     both read `ss%tau_x` raw — and refreshes `stress_mag`, so
         !     KPP/EPBL `u*` is zero-wind under cover;
         !   * `q_heat`/`q_salt`, the radiative/turbulent bands, the
         !     mass-flux enthalpies and `salt_flux` are masked in
         !     `ocean_surface_flux_assemble` (cover-aware twin), which is
         !     the one place they are still separable from the cavity's
         !     own `heat_cavity`/`salt_cavity` — and which makes the
         !     `Q_heat`/`Q_salt` KPP/EPBL read for `B_0` the MASKED
         !     values;
         !   * shortwave penetration and surface restoring carry the
         !     factor themselves (they do not route through `Q_*`).
         !
         ! `&ocean_psurf_nml enable` was never refused here: the cavity
         ! is the `ms%p_top` producer (P5.2: `p_ice_ref + sf%p_surf`), so
         ! the seam composes with the ice load rather than clobbering it.
         if (cfg%ocean%ice%enable) then
            call logger%error("&ocean_cavity_melt_nml enable=.true. is mutually "// &
                              "exclusive with &ocean_ice_nml enable=.true.  Sea ice "// &
                              "full-overwrites heat_added/salt_flux and runs its own "// &
                              "instant-relaxation basal flux with a DIFFERENT "// &
                              "liquidus set; two interface thermodynamics in one "// &
                              "column is not a configuration.")
            has_error = .true.
         end if
         ! (E4) Constant α/β under a NONLINEAR EOS in a cavity — a WARNING,
         ! deliberately, not a refusal.  ISOMIP+ prescribes the LINEAR EOS
         ! (Asay-Davis et al. 2016 Table 4), and there the constants ARE
         ! that EOS's exact coefficients, so every shipped cavity namelist
         ! is unaffected and must keep running untouched.  Under Wright or
         ! Roquet the pair is a constant stand-in for a coefficient that
         ! collapses toward zero at the freezing point and roughly doubles
         ! by 1000 dbar — exactly the corner a cavity sits in.
         if (trim(adjustl(cfg%ocean%vmix%buoyancy_coeffs)) == "constant" .and. &
             trim(adjustl(cfg%ocean%eos%eos)) /= "linear") then
            call logger%warning("&ocean_cavity_melt_nml enable=.true. with a "// &
                                "NONLINEAR EOS (&ocean_eos_nml eos='"// &
                                trim(adjustl(cfg%ocean%eos%eos))//"') but "// &
                                "&ocean_vmix_nml buoyancy_coeffs='constant'.  The "// &
                                "KPP surface buoyancy flux B_0 and the "// &
                                "double-diffusion density ratio will size the "// &
                                "melt-driven buoyancy with the SCALAR "// &
                                "&ocean_ic_nml alpha_T/beta_S, not with the "// &
                                "derivatives of the density this run actually "// &
                                "integrates.  Near the freezing point thermal "// &
                                "expansion is several times smaller than at 10 degC "// &
                                "and grows strongly with pressure, so the boundary "// &
                                "layer under the shelf can be mis-sized (and in the "// &
                                "cold-fresh corner mis-signed).  Set "// &
                                "buoyancy_coeffs='eos' unless you are reproducing a "// &
                                "constant-coefficient reference.")
         end if
      end if
      ! ---- Ice-shelf TOP drag (&ocean_tdrag_nml, Phase 4a) ----
      ! Default off ⇒ this whole block is skipped and every path is
      ! bit-identical.  Every restriction below fails loud: a top drag
      ! that is silently inert (no cover, no coefficient) is worse than
      ! no top drag, because a cavity circulation would then be
      ! frictionless at the ice and LOOK like it was damped.
      if (cfg%ocean%tdrag%enable) then
         if (.not. cfg%ocean%cavity_dyn%enable) then
            call logger%error("&ocean_tdrag_nml enable=.true. requires "// &
                              "&ocean_cavity_dyn_nml enable=.true.  The top drag "// &
                              "stands on that group's geometry: without a draft "// &
                              "there is no ice base, cover_frac is identically zero, "// &
                              "and every face mask would be zero — an inert kernel "// &
                              "with a cost.  An OPEN surface's momentum boundary "// &
                              "condition is the wind stress (&ocean_topo_nml "// &
                              "wind_config), not a drag.")
            has_error = .true.
         end if
         block
            integer :: tdrag_code
            tdrag_code = parse_tdrag_variant(cfg%ocean%tdrag%form)
            if (.not. tdrag_variant_is_implemented(tdrag_code)) then
               call logger%error("&ocean_tdrag_nml form = '"// &
                                 trim(adjustl(cfg%ocean%tdrag%form))// &
                                 "' is not recognised (quadratic|linear).  The two "// &
                                 "forms take coefficients of DIFFERENT dimensions "// &
                                 "(cd dimensionless, r in 1/s), so a typo cannot be "// &
                                 "defaulted.")
               has_error = .true.
            else if (tdrag_code == TDRAG_QUADRATIC .and. cfg%ocean%tdrag%cd <= 0.0_wp) then
               call logger%error("&ocean_tdrag_nml form='quadratic' requires cd > 0 "// &
                                 "(ISOMIP+ prescribes 2.5e-3).  cd = 0 is an "// &
                                 "identically zero drag, which is what enable=.false. "// &
                                 "is for.")
               has_error = .true.
            else if (tdrag_code == TDRAG_LINEAR .and. cfg%ocean%tdrag%r <= 0.0_wp) then
               call logger%error("&ocean_tdrag_nml form='linear' requires r > 0 "// &
                                 "(1/s).  r = 0 is an identically zero drag, which "// &
                                 "is what enable=.false. is for.")
               has_error = .true.
            end if
            ! ---- ONE drag coefficient for momentum and melt ----
            ! MOM6 carries two independent top-drag coefficients (one in
            ! the momentum BC, one in the melt u*); we deliberately do
            ! not.  A cavity in which the ice base takes momentum out of
            ! the flow at one C_d and reports a friction velocity built
            ! on another is not a closure, it is two closures sharing a
            ! boundary.  So: when both groups are on, the melt slot TAKES
            ! its C_d from this group (`configure_ocean_cavity_melt`), and
            ! a user who set both to DIFFERENT values is told rather than
            ! silently overridden.
            if (cfg%ocean%cavity_melt%enable .and. tdrag_code == TDRAG_QUADRATIC) then
               if (cfg%ocean%cavity_melt%cdrag_top /= cfg%ocean%tdrag%cd) then
                  call logger%error("&ocean_tdrag_nml cd = "// &
                                    to_string(cfg%ocean%tdrag%cd)//" and "// &
                                    "&ocean_cavity_melt_nml cdrag_top = "// &
                                    to_string(cfg%ocean%cavity_melt%cdrag_top)// &
                                    " disagree.  There is ONE ice-base drag "// &
                                    "coefficient in this model: the same C_d sets "// &
                                    "the momentum sink and the melt friction "// &
                                    "velocity u* = sqrt(C_d*(U^2 + u_tide^2)).  Set "// &
                                    "them equal (or leave cdrag_top at its default "// &
                                    "and set &ocean_tdrag_nml cd alone).")
                  has_error = .true.
               end if
            end if
            if (cfg%ocean%cavity_melt%enable .and. tdrag_code == TDRAG_LINEAR) then
               call logger%warning("&ocean_tdrag_nml form='linear' with "// &
                                   "&ocean_cavity_melt_nml enable=.true.: the melt "// &
                                   "friction velocity is quadratic by construction "// &
                                   "(u* = sqrt(C_d*(U^2 + u_tide^2))), so it keeps "// &
                                   "its own cdrag_top and the momentum sink uses r. "// &
                                   "The two boundary conditions are then NOT the "// &
                                   "same closure — intended for analytic work only.")
            end if
         end block
      end if
      ! ---- Dynamic wetting/drying v1 scope (docs/ocean_wetdry_plan.md §6) ----
      ! Every restriction fails loud: silently running wet/dry outside its
      ! validated envelope is the coastal ZSTAR_FULL salt-leak foot-gun class.
      if (cfg%ocean%wetdry%enable) then
         if (.not. (cfg%ocean%wetdry%rewet_depth > cfg%ocean%wetdry%dry_depth &
                    .and. cfg%ocean%wetdry%dry_depth > 0.0_wp)) then
            call logger%error("&ocean_wetdry_nml requires rewet_depth > dry_depth > 0 "// &
                              "(hysteresis band; equal thresholds flip-flop the front)")
            has_error = .true.
         end if
         ! v1 vertical-coordinate restriction: sigma only.  On sigma the
         ! layer partition scales all layers to zero TOGETHER as D -> 0, so
         ! a dry column is just "all layers vanished" (a state the
         ! H_VANISHED gates already handle).  ZSTAR_FULL bed layers pinch
         ! independently of the surface — the argument fails and the
         ! documented 1-2%/cycle intertidal salt leak would compound.
         ! 'zstar' was accepted while it was sigma under another name; as
         ! MOM6 z* it carries zstar_h_min bed fillers (below H_VANISHED)
         ! where the wet/dry seed and floor assume an emerged column of
         ! nz*2*H_VANISHED, and its dilation is floored at a dry column —
         ! unvalidated, so refused.
         ! Parse (not string-compare): the vcoord string has aliases
         ! ("z-star", "SIGMA", ...); same ocean default as rdb_ocean_vcoord.
         block
            integer :: wd_vcoord_code
            wd_vcoord_code = parse_vcoord_type(cfg%vcoord_type, &
                                               default_code=VCOORD_EULERIAN_Z)
            if (.not. (wd_vcoord_code == VCOORD_SIGMA)) then
               call logger%error("&ocean_wetdry_nml enable=.true. supports "// &
                                 "vcoord_type='sigma' only — got '"// &
                                 trim(cfg%vcoord_type)//"' (ZSTAR_FULL bed layers "// &
                                 "pinch independently; 'zstar' is MOM6 z* with bed "// &
                                 "fillers below the wet/dry emerged-column floor; "// &
                                 "other coords unvalidated)")
               has_error = .true.
            end if
         end block
         ! Positive-definite continuity composes its own per-layer outflux
         ! limiter; wet/dry owns the barotropic drying limiter.  The two
         ! positivity machineries have not been reconciled (v1) — fail loud
         ! rather than run both over the same faces.
         if (cfg%ocean%continuity%positive_definite) then
            call logger%error("&ocean_wetdry_nml enable=.true. is mutually "// &
                              "exclusive with &ocean_continuity_nml "// &
                              "positive_definite=.true. (wet/dry owns its own "// &
                              "barotropic limiter; composition deferred)")
            has_error = .true.
         end if
         ! Layer-side positivity is the continuity-PPM limiter's job — the BT
         ! limiter bounds the column, ppm_limit_pos bounds each layer.
         if (.not. cfg%ocean%continuity%ppm_limit_pos) then
            call logger%error("&ocean_wetdry_nml enable=.true. requires "// &
                              "&ocean_continuity_nml ppm_limit_pos=.true. "// &
                              "(per-layer positivity under drying transports)")
            has_error = .true.
         end if
         ! The BT_cont / upstream-h Pass-1 flux branches are not composed
         ! with the wet/dry upwind+limiter branch in v1 (each replaces the
         ! same face-thickness logic).
         if (cfg%ocean%bt%use_cont_type .or. cfg%ocean%bt%upstream_h_face) then
            call logger%error("&ocean_wetdry_nml enable=.true. is mutually "// &
                              "exclusive with &ocean_bt_nml use_cont_type / "// &
                              "upstream_h_face (all three replace the BT Pass-1 "// &
                              "face thickness; composition deferred)")
            has_error = .true.
         end if
         ! v1 single-rank: the theta/mask ghost exchange lands with the
         ! C-grid MPI halo work.
         if (cfg%px*cfg%py > 1) then
            call logger%error("&ocean_wetdry_nml enable=.true. is single-rank in "// &
                              "v1 (px*py = 1); the wet-mask/limiter halo exchange "// &
                              "is deferred to the C-grid MPI work")
            has_error = .true.
         end if
         ! Wet/dry needs the split solver: the limiter lives in the BT substep.
         if (cfg%ocean%bt%n_inner < 1 .and. .not. cfg%ocean%bt%auto_n_inner) then
            call logger%error("&ocean_wetdry_nml enable=.true. requires the "// &
                              "split-explicit solver (&ocean_bt_nml n_inner >= 1 "// &
                              "or auto_n_inner=.true.)")
            has_error = .true.
         end if
         ! Surface-flux composition: only the MAIN heat/salt deposit is
         ! dynamic-mask gated in v1.  sw_pen removes the surface deposit
         ! and re-adds a distributed profile — on a masked (dry) column
         ! it would remove heat that was never added; the restoring
         ! piston rate lambda = piston/h_top and geothermal's bed deposit
         ! blow up on a ~0-thickness sliver.  All three mis-compose:
         ! fail loud, defer the gated variants.
         if (cfg%ocean%thermo%sw_pen_frac > 0.0_wp) then
            call logger%error("&ocean_wetdry_nml enable=.true. does not yet "// &
                              "compose with shortwave penetration "// &
                              "(&ocean_thermo_nml sw_pen_frac > 0): the additive "// &
                              "correction would withdraw a surface deposit the "// &
                              "dry-column mask never made")
            has_error = .true.
         end if
         if (cfg%ocean%restore%enable_restore_temp .or. &
             cfg%ocean%restore%enable_restore_salt) then
            call logger%error("&ocean_wetdry_nml enable=.true. does not yet "// &
                              "compose with surface restoring (&ocean_restore_nml): "// &
                              "the piston rate lambda = piston/h_top diverges on a "// &
                              "drying column")
            has_error = .true.
         end if
         if (cfg%ocean%geothermal%enable) then
            call logger%error("&ocean_wetdry_nml enable=.true. does not yet "// &
                              "compose with geothermal heating "// &
                              "(&ocean_geothermal_nml): the bed deposit blows up "// &
                              "a dried column's vanished bed layer")
            has_error = .true.
         end if
      end if
      ! ---- Pseudo-salt: fail-loud exclusions ----
      ! Pseudo-salt's contract is "receives EXACTLY the operators salinity
      ! receives" (PR-28).  SSS piston restoring and sea-ice frazil/basal
      ! salt exchange are salinity sources this PR does NOT mirror into
      ! pseudo-salt — an un-mirrored source would make the deviation
      ! diagnostic (pseudo_salt - S) measure that source instead of the
      ! passive-transport-path error it exists to isolate.  Abort rather
      ! than silently ship a lying diagnostic.
      if (pseudo_salt_conflicts_restore(cfg%ocean%tracers%enable_pseudo_salt, &
                                        cfg%ocean%restore%enable_restore_salt)) then
         call logger%error("&ocean_tracers_nml enable_pseudo_salt=.true. does not "// &
                           "compose with &ocean_restore_nml enable_restore_salt=.true. "// &
                           "(SSS piston restoring): the deviation diagnostic would "// &
                           "measure the un-mirrored restoring term, not the "// &
                           "passive-transport-path error")
         has_error = .true.
      end if
      if (pseudo_salt_conflicts_ice(cfg%ocean%tracers%enable_pseudo_salt, &
                                    cfg%ocean%ice%enable)) then
         call logger%error("&ocean_tracers_nml enable_pseudo_salt=.true. does not "// &
                           "compose with &ocean_ice_nml enable=.true.: sea-ice "// &
                           "frazil/basal salt exchange is an un-mirrored salinity "// &
                           "source that would corrupt the deviation diagnostic")
         has_error = .true.
      end if
      if (pseudo_salt_needs_thermo_warning(cfg%ocean%tracers%enable_pseudo_salt, &
                                           cfg%ocean%thermo%enable_thermodynamics)) then
         call logger%warning("&ocean_tracers_nml enable_pseudo_salt=.true. with "// &
                             "&ocean_thermo_nml enable_thermodynamics=.false.: "// &
                             "pseudo-salt will never receive the surface salt flux "// &
                             "or KPP non-local mirrors, so it measures pure "// &
                             "advection/diffusion transport only — a legitimate "// &
                             "but narrower use of the diagnostic")
      end if
      ! ---- Sea-ice: whole-slot envelope (applies to enable=.true.) ----
      ! The sea ice runs on the ocean's decomposition: the category state,
      ! the EVP ice velocity and the blended surface stress are
      ! halo-exchanged (`engine_step_ice`).  The ice fields are NOT folded
      ! across a tripolar north seam, so a folded grid stays single-rank.
      ! (The engine repeats this on the ACTUAL rank count, which an unset
      ! process grid does not show here.)
      if (cfg%ocean%ice%enable) then
         if (cfg%px*cfg%py > 1 .and. trim(cfg%ocean%bc%north) == "tripolar_fold") then
            call logger%error("&ocean_ice_nml enable=.true. with north='tripolar_fold' "// &
                              "is single-rank (px*py = 1): the ice fields are not "// &
                              "folded across the north seam")
            has_error = .true.
         end if
         ! Sea ice is tuned for global, ice-covered basins — not for
         ! regional/coastal runs.  It runs every outer step and is NOT
         ! performance-tuned; expect a large throughput hit.  Warn loudly
         ! so it is never mistaken for a free rider on a coastal run.
         call logger%warning("=====================================================")
         call logger%warning("&ocean_ice_nml enable=.true.: SEA ICE IS ON.")
         call logger%warning("Sea ice is tuned for GLOBAL, ice-covered simulations "// &
                             "and is NOT performance-tuned. Expect it to be VERY "// &
                             "SLOW on regional/coastal domains where most of the "// &
                             "grid is ice-free.")
         call logger%warning("=====================================================")
      end if
      ! ---- Sea-ice PR 4b: category ice transport envelope ----
      ! `transport=.true.` is a request to run `ice_transport_step` every
      ! thermo window; the v1 envelope excludes configurations the kernel
      ! does not (yet) handle — fail loud rather than silently produce a
      ! wrong/unstable answer.
      if (cfg%ocean%ice%transport) then
         if (.not. cfg%ocean%ice%enable) then
            call logger%error("&ocean_ice_nml transport=.true. requires "// &
                              "enable=.true. (the ice slot must be live)")
            has_error = .true.
         end if
         if (cfg%ocean%ice%ncat <= 1) then
            call logger%error("&ocean_ice_nml transport=.true. requires ncat > 1 "// &
                              "(ncat==1 is the frozen legacy lumped mode — no ITD, "// &
                              "nothing to transport by category)")
            has_error = .true.
         end if
         ! The driver only reaches `ice_transport_step` inside the
         ! `enable_thermodynamics .and. is_thermo_step()` ice block, so
         ! `transport=.true.` with thermo off is a WORDLESS no-op — fail
         ! loud (house rule) instead.
         if (.not. cfg%ocean%thermo%enable_thermodynamics) then
            call logger%error("&ocean_ice_nml transport=.true. requires "// &
                              "&ocean_thermo_nml enable_thermodynamics=.true. "// &
                              "(the transport call fires only on the thermo cadence)")
            has_error = .true.
         end if
         ! Periodic edges and more than one rank are both fine: the CAS
         ! masses and riding tracers are halo-exchanged (or, on one rank,
         ! wrapped) at the top of every advective substep, and a seam face
         ! is never zeroed as a wall (`rdb_ice_transport`).
         if (cfg%ocean%ice%adv_substeps < 1) then
            call logger%error("&ocean_ice_nml adv_substeps must be >= 1")
            has_error = .true.
         end if
      end if

      ! ---- Sea-ice PR 5: C-grid EVP dynamics envelope ----
      ! `dynamics=.true.` requests `ice_evp_step` every outer step; the v1
      ! envelope excludes configurations the kernel does not (yet) handle —
      ! fail loud rather than silently produce a wrong/unstable answer.
      ! EVP allows periodic edges (the ghost-wrap machinery in `rdb_ice_evp`
      ! mirrors the ocean's own periodic-wrap contract, as transport now
      ! does too); its edge envelope is its own, not a re-use of the
      ! transport block above.
      ! Multi-rank: the ice velocity is halo-exchanged every subcycle.
      if (cfg%ocean%ice%dynamics) then
         if (.not. cfg%ocean%ice%enable) then
            call logger%error("&ocean_ice_nml dynamics=.true. requires "// &
                              "enable=.true. (the ice slot must be live)")
            has_error = .true.
         end if
         ! v1 envelope: every edge must be WALL or PERIODIC — no OBC/tidal/
         ! sponge/clamped/Chapman edges for ice (the momentum solve has no
         ! notion of an open ice boundary yet).
         if (.not. any(ocean_bc_type_from_string(cfg%ocean%bc%west) == &
                       [OBC_WALL, OBC_PERIODIC]) .or. &
             .not. any(ocean_bc_type_from_string(cfg%ocean%bc%east) == &
                       [OBC_WALL, OBC_PERIODIC]) .or. &
             .not. any(ocean_bc_type_from_string(cfg%ocean%bc%south) == &
                       [OBC_WALL, OBC_PERIODIC]) .or. &
             .not. any(ocean_bc_type_from_string(cfg%ocean%bc%north) == &
                       [OBC_WALL, OBC_PERIODIC])) then
            call logger%error("&ocean_ice_nml dynamics=.true. requires every "// &
                              "&ocean_bc_nml edge to be 'wall' or 'periodic' (v1: no "// &
                              "OBC/tidal/sponge/clamped/Chapman edges for ice dynamics)")
            has_error = .true.
         end if
         ! No tripolar north fold: the EVP ghost-wrap contract is plain
         ! periodic/wall only.
         if (trim(cfg%ocean%bc%north) == "tripolar_fold") then
            call logger%error("&ocean_ice_nml dynamics=.true. is incompatible with "// &
                              "north='tripolar_fold' (v1: plain periodic/wall ghost "// &
                              "policy only)")
            has_error = .true.
         end if
         if (cfg%ocean%ice%evp_sub_steps < 1) then
            call logger%error("&ocean_ice_nml evp_sub_steps must be >= 1")
            has_error = .true.
         end if
         ! Physical-parameter positivity guards — fail loud on a typo/garbage
         ! value that would silently produce a wrong or NaN rheology. `ec`
         ! allows 0 (the documented cavitating-fluid mode: `i_ec2` is forced
         ! to 0 in `rdb_ice_evp`), so it is guarded `>= 0`, not `> 0`.
         ! `tdamp` is legitimately sign-free (`< 0` selects the
         ! fraction-of-slow-step form) — no guard.
         if (cfg%ocean%ice%p0 <= 0.0_wp) then
            call logger%error("&ocean_ice_nml p0 (ice-strength P*) must be > 0")
            has_error = .true.
         end if
         if (cfg%ocean%ice%c0 <= 0.0_wp) then
            call logger%error("&ocean_ice_nml c0 (ice-strength C*) must be > 0")
            has_error = .true.
         end if
         if (cfg%ocean%ice%ec < 0.0_wp) then
            call logger%error("&ocean_ice_nml ec (yield ellipticity) must be >= 0 "// &
                              "(0 = cavitating-fluid rheology)")
            has_error = .true.
         end if
         if (cfg%ocean%ice%cdw <= 0.0_wp) then
            call logger%error("&ocean_ice_nml cdw (ice-ocean drag coefficient) must be > 0")
            has_error = .true.
         end if
         if (cfg%ocean%ice%rho_ocean <= 0.0_wp) then
            call logger%error("&ocean_ice_nml rho_ocean (ice-drag reference density) "// &
                              "must be > 0")
            has_error = .true.
         end if
         if (cfg%ocean%ice%del_sh_min_scale <= 0.0_wp) then
            call logger%error("&ocean_ice_nml del_sh_min_scale (viscosity-floor scale) "// &
                              "must be > 0")
            has_error = .true.
         end if
         ! Drift-only mode (dynamics without transport) has no CFL backstop
         ! on u_ice: transport's positivity abort is the only ice-velocity
         ! guard. Warn (NOT error — the envelope is unchanged); this
         ! combination is unvalidated.
         if (.not. cfg%ocean%ice%transport) then
            call logger%warning("&ocean_ice_nml dynamics=.true. without "// &
                                "transport=.true.: drift-only mode has no CFL backstop "// &
                                "on u_ice (transport's positivity abort is the only "// &
                                "ice-velocity guard) and is unvalidated")
         end if
         ! PR 36: the per-iteration clip needs a ceiling to clip to — SIS2's
         ! own triple gate (`do_trunc_its = CFL_check_its .and. CFL_trunc>0
         ! .and. dt_slow>0`) makes this combination a silent no-op; fail
         ! loud instead of silently reproducing SIS2's silence.
         if (cfg%ocean%ice%cfl_trunc_dyn_its .and. cfg%ocean%ice%cfl_trunc <= 0.0_wp) then
            call logger%error("&ocean_ice_nml cfl_trunc_dyn_its=.true. requires "// &
                              "cfl_trunc > 0 (the per-iteration check needs a ceiling)")
            has_error = .true.
         end if
         ! SIS2 permits cfl_trunc > 1 but calls it unwise ("instability can
         ! occur past 0.5") — warn, do not fail loud.
         if (cfg%ocean%ice%cfl_trunc > 1.0_wp) then
            call logger%warning("&ocean_ice_nml cfl_trunc > 1.0 is permitted but unwise "// &
                                "(SIS2: 'instability can occur past 0.5')")
         end if
      end if

      ! ---- Sea-ice PR 36: cfl_trunc / cfl_trunc_dyn_its / project_ci ----
      ! require dynamics; a knob that only has meaning inside a disabled
      ! feature is a wordless no-op — fail loud instead (same pattern as
      ! a_face_stress below). Deliberately OUTSIDE the `if (dynamics)`
      ! envelope above: it must fire precisely when `dynamics` is false.
      if (cfg%ocean%ice%cfl_trunc > 0.0_wp .and. .not. cfg%ocean%ice%dynamics) then
         call logger%error("&ocean_ice_nml cfl_trunc > 0 requires dynamics=.true. "// &
                           "(the knob only reaches the EVP velocity solve)")
         has_error = .true.
      end if
      if (cfg%ocean%ice%cfl_trunc_dyn_its .and. .not. cfg%ocean%ice%dynamics) then
         call logger%error("&ocean_ice_nml cfl_trunc_dyn_its=.true. requires "// &
                           "dynamics=.true. (the knob only reaches the EVP velocity solve)")
         has_error = .true.
      end if
      if (cfg%ocean%ice%project_ci .and. .not. cfg%ocean%ice%dynamics) then
         call logger%error("&ocean_ice_nml project_ci=.true. requires dynamics=.true. "// &
                           "(the knob only reaches the EVP subcycle loop)")
         has_error = .true.
      end if

      ! ---- Map-driven sponge (&ocean_sponge_nml, PR-23) ----
      if (cfg%ocean%sponge%enable) then
         if (.not. sponge_source_is_implemented(cfg%ocean%sponge%damp_source)) then
            call logger%error("&ocean_sponge_nml enable=.true. with unimplemented "// &
                              "damp_source = '"//trim(cfg%ocean%sponge%damp_source)// &
                              "': must be 'band' ('file' is PR-23b, needs the PR-14 reader)")
            has_error = .true.
         end if
         if (.not. sponge_target_is_implemented(cfg%ocean%sponge%target_source)) then
            call logger%error("&ocean_sponge_nml enable=.true. with unimplemented "// &
                              "target_source = '"//trim(cfg%ocean%sponge%target_source)// &
                              "': must be 'ic' or 'linear_z' "// &
                              "('file' is PR-23b, needs the PR-14 reader)")
            has_error = .true.
         end if
         if (.not. sponge_ramp_is_valid(cfg%ocean%sponge%ramp)) then
            call logger%error("&ocean_sponge_nml ramp = '"//trim(cfg%ocean%sponge%ramp)// &
                              "': must be 'cosine' (default, the legacy band shape) "// &
                              "or 'linear' (ISOMIP+ Eq. 20)")
            has_error = .true.
         end if
         ! A `linear_z` target with both gradients AND both references left
         ! at their defaults would relax toward T = 0 degC / S = 35 PSU
         ! everywhere — almost certainly not what was meant, and silent.
         if (trim(cfg%ocean%sponge%target_source) == "linear_z" .and. &
             cfg%ocean%sponge%lin_t_ref == 0.0_wp .and. &
             cfg%ocean%sponge%lin_dt_dz == 0.0_wp .and. &
             cfg%ocean%sponge%lin_ds_dz == 0.0_wp) then
            call logger%warning("&ocean_sponge_nml target_source='linear_z' with "// &
                                "lin_t_ref = lin_dt_dz = lin_ds_dz = 0: the sponge will "// &
                                "relax toward a uniform T = 0 degC column. Set the "// &
                                "lin_* profile knobs.")
         end if
         ! relax_h (interior-interface thickness damping) is NOT implemented
         ! in PR-23 v1 — deferred to PR-23b alongside the file targets (the
         ! plan's §14 Q1 recommendation). Abort unconditionally rather than
         ! silently ignore the knob.
         if (cfg%ocean%sponge%relax_h) then
            call logger%error("&ocean_sponge_nml relax_h=.true. is not implemented "// &
                              "in PR-23 v1 (interior-interface thickness damping is "// &
                              "deferred to PR-23b; see docs/plans/PLAN_PR23_real_sponge.md "// &
                              "§14 Q1) — leave relax_h=.false.")
            has_error = .true.
         end if
         ! The sponge is dispatched only from run_stage_split (the split
         ! barotropic solver); the unsplit run_stage never receives bc or sp.
         ! With auto_n_inner=.true. n_inner resolves later at setup, so only
         ! a HARD n_inner < 1 with auto off is an error here (mirrors the
         ! &ocean_wetdry_nml n_inner precedent above).
         if (cfg%ocean%bt%n_inner < 1 .and. .not. cfg%ocean%bt%auto_n_inner) then
            call logger%error("&ocean_sponge_nml enable=.true. requires the "// &
                              "split-explicit solver (&ocean_bt_nml n_inner >= 1 "// &
                              "or auto_n_inner=.true.) — the sponge is dispatched only "// &
                              "from the split barotropic path")
            has_error = .true.
         end if
         ! A "band" idamp source with no OBC_SPONGE-tagged edge would build
         ! an all-zero map — the sponge would silently do nothing.
         if (trim(cfg%ocean%sponge%damp_source) == "band") then
            if (trim(cfg%ocean%bc%west) /= "sponge" .and. &
                trim(cfg%ocean%bc%east) /= "sponge" .and. &
                trim(cfg%ocean%bc%south) /= "sponge" .and. &
                trim(cfg%ocean%bc%north) /= "sponge") then
               call logger%error("&ocean_sponge_nml enable=.true., damp_source='band' "// &
                                 "requires at least one &ocean_bc_nml edge = 'sponge' "// &
                                 "(otherwise idamp_h/u/v would build all-zero)")
               has_error = .true.
            end if
         end if
         ! Per-edge width/strength overrides: sentinel is < 0 (inherit); a
         ! non-sentinel width must be non-negative, and a positive width
         ! paired with a zero strength is a silent no-op sponge.
         if (cfg%ocean%sponge%west_width < -1 .or. cfg%ocean%sponge%east_width < -1 .or. &
             cfg%ocean%sponge%south_width < -1 .or. cfg%ocean%sponge%north_width < -1) then
            call logger%error("&ocean_sponge_nml *_width overrides must be >= -1 "// &
                              "(-1 = inherit &ocean_bc_nml sponge_width)")
            has_error = .true.
         end if
         if ((cfg%ocean%sponge%west_width > 0 .and. cfg%ocean%sponge%west_strength == 0.0_wp) .or. &
             (cfg%ocean%sponge%east_width > 0 .and. cfg%ocean%sponge%east_strength == 0.0_wp) .or. &
             (cfg%ocean%sponge%south_width > 0 .and. cfg%ocean%sponge%south_strength == 0.0_wp) .or. &
             (cfg%ocean%sponge%north_width > 0 .and. cfg%ocean%sponge%north_strength == 0.0_wp)) then
            call logger%error("&ocean_sponge_nml a positive *_width override with "// &
                              "*_strength == 0.0 would build an all-zero band on that "// &
                              "edge — set a nonzero *_strength or leave *_width at -1")
            has_error = .true.
         end if
      end if
      ! ---- Barotropic linear wave drag (&ocean_bt_nml wave_drag) ----
      ! `wave_drag_form` is an `nml_enum` (allowed = uniform/roughness_proxy/
      ! file), so a garbage tag is already rejected at nml-apply time; the
      ! one form-specific case left to catch here is "file", which is
      ! REGISTERED but has no reader yet (PR-14) — fail loud rather than
      ! silently no-op (the audit's trap #12: a named-but-dead option).
      if (trim(cfg%ocean%bt%wave_drag_form) == "file") then
         call logger%error("&ocean_bt_nml wave_drag_form='file' is not "// &
                           "implemented — the NetCDF map reader is PR-14; "// &
                           "use wave_drag_form='uniform' or 'roughness_proxy'")
         has_error = .true.
      end if
      if (cfg%ocean%bt%wave_drag_scale < 0.0_wp) then
         call logger%error("&ocean_bt_nml wave_drag_scale must be >= 0 "// &
                           "(negative r_H makes bt_rem > 1, exponentially "// &
                           "growing the barotropic mode)")
         has_error = .true.
      end if
      if (cfg%ocean%bt%wave_drag_r_uniform < 0.0_wp) then
         call logger%error("&ocean_bt_nml wave_drag_r_uniform must be >= 0")
         has_error = .true.
      end if
      if (cfg%ocean%bt%wave_drag_kappa <= 0.0_wp) then
         call logger%error("&ocean_bt_nml wave_drag_kappa must be > 0")
         has_error = .true.
      end if
      if (cfg%ocean%bt%wave_drag_n_bot < 0.0_wp) then
         call logger%error("&ocean_bt_nml wave_drag_n_bot must be >= 0")
         has_error = .true.
      end if
      if (cfg%ocean%bt%wave_drag_h2_max <= 0.0_wp) then
         call logger%error("&ocean_bt_nml wave_drag_h2_max must be > 0")
         has_error = .true.
      end if
      if (cfg%ocean%bt%wave_drag) then
         if (trim(cfg%ocean%bt%wave_drag_form) == "uniform" .and. &
             cfg%ocean%bt%wave_drag_r_uniform <= 0.0_wp) then
            call logger%warning("&ocean_bt_nml wave_drag=.true. with "// &
                                "wave_drag_form='uniform' and wave_drag_r_uniform <= 0 "// &
                                "=> inert (r_H == 0 everywhere)")
         end if
         if (trim(cfg%ocean%bt%wave_drag_form) == "roughness_proxy") then
            call logger%warning("&ocean_bt_nml wave_drag_form='roughness_proxy' derives "// &
                                "<h^2> from RESOLVED bathymetry variance — it is a "// &
                                "placeholder for a real subgrid roughness map "// &
                                "(PR-14/PR-30), not a substitute")
         end if
      end if
      ! ---- Sea-ice PR-58: ITD category-bound override envelope ----
      ! `hlim=...` is consumed entirely inside `ice%init` (called from
      ! `ocean_state_init_from_config`, BEFORE the thermo cadence), so —
      ! unlike transport/dynamics — there is no `enable_thermodynamics`
      ! clause to copy here: a wordless no-op would require the value to
      ! be read on a cadence it never reaches, and `hlim` has no such gate.
      if (ice_hlim_count(cfg%ocean%ice%hlim) > 0) then
         if (.not. cfg%ocean%ice%enable) then
            call logger%error("&ocean_ice_nml hlim=... requires enable=.true. "// &
                              "(the ice slot must be live for the override to apply)")
            has_error = .true.
         end if
         block
            logical :: hlim_ok
            character(len=:), allocatable :: reason
            call ice_hlim_spec_is_valid(cfg%ocean%ice%hlim, cfg%ocean%ice%ncat, hlim_ok, reason)
            if (.not. hlim_ok) then
               call logger%error("&ocean_ice_nml hlim: "//reason)
               has_error = .true.
            end if
         end block
      end if
      ! ---- Sea-ice PR 24: analytic initial-condition envelope ----
      ! `conc_config /= "zero"` is a request to seed a live ice pack
      ! before the first step; the envelope excludes configurations the
      ! seeder cannot handle consistently — fail loud rather than
      ! silently seed a meaningless or self-melting pack.
      if (trim(cfg%ocean%ice_ic%conc_config) /= "zero") then
         ! V1: mirrors "transport=.true. requires enable=.true." above.
         if (.not. cfg%ocean%ice%enable) then
            call logger%error("&ocean_ice_ic_nml conc_config /= 'zero' requires "// &
                              "&ocean_ice_nml enable=.true. (the ice slot must be live)")
            has_error = .true.
         end if
         ! V2: an IC with no thickness is a wordless no-op; h_ice > 0
         ! also keeps the pack above h_lim(1) = 1e-10 m so the ITD cat-1
         ! compress cannot fire on the very first thermo window.
         if (cfg%ocean%ice_ic%h_ice <= 0.0_wp) then
            call logger%error("&ocean_ice_ic_nml conc_config /= 'zero' requires "// &
                              "h_ice > 0 (an IC with no thickness is a wordless no-op, "// &
                              "and h_ice must clear the category-1 floor)")
            has_error = .true.
         end if
         ! V3: mirrors the "wordless no-op => fail loud" argument used
         ! for transport above.
         if (trim(cfg%ocean%ice_ic%conc_config) == "uniform" .and. &
             cfg%ocean%ice_ic%conc <= 0.0_wp) then
            call logger%error("&ocean_ice_ic_nml conc_config='uniform' requires "// &
                              "conc > 0 (conc <= 0 is a wordless open-water no-op)")
            has_error = .true.
         end if
         ! V4: copy of the equilibrium-tide cartesian-grid guard above —
         ! geolatT/geolonT stay 0 on a cartesian grid (only
         ! metrics_init_spherical fills them), so a latitudes IC would
         ! silently ice the whole basin or none of it.
         if (trim(cfg%ocean%ice_ic%conc_config) == "latitudes" .and. &
             trim(cfg%ocean%grid%grid_config) == "cartesian") then
            call logger%error("&ocean_ice_ic_nml conc_config='latitudes' requires a "// &
                              "non-cartesian &ocean_grid_nml grid_config (spherical/"// &
                              "tripolar/supergrid) — geolatT is meaningless on cartesian")
            has_error = .true.
         end if
         ! V5: the legacy lumped mode (ncat==1) has no partial-cover
         ! representation — ice_cell_concentration_impl returns a BINARY
         ! ci, so a fractional conc would be silently reinterpreted.
         if (cfg%ocean%ice%ncat == 1 .and. &
             trim(cfg%ocean%ice_ic%conc_config) == "uniform" .and. &
             cfg%ocean%ice_ic%conc /= 1.0_wp) then
            call logger%error("&ocean_ice_ic_nml conc_config='uniform' with "// &
                              "&ocean_ice_nml ncat==1 requires conc == 1.0 (the legacy "// &
                              "lumped mode has no partial-cover representation — "// &
                              "ice_cell_concentration_impl returns a BINARY ci)")
            has_error = .true.
         end if
         ! V6: at/above the liquidus, ice_enth_from_ts returns the
         ! LIQUID-WATER branch — the pack would carry m_ice > 0 whose
         ! enthalpy says "this is water" and melt out on the first
         ! thermo step, silently.
         if (cfg%ocean%ice_ic%t_ice >= ice_t_freeze(cfg%ocean%ice_ic%s_ice)) then
            call logger%error("&ocean_ice_ic_nml conc_config /= 'zero' requires "// &
                              "t_ice < the liquidus at s_ice (t_ice >= t_freeze(s_ice) "// &
                              "silently yields LIQUID-WATER enthalpy — the pack would "// &
                              "melt out on the first thermo step)")
            has_error = .true.
         end if
         ! V7: orphan snow. The column entry gate is on m_ice, and
         ! ice_adjust_categories' massless-cleanup step would silently
         ! absorb snow-with-no-ice into the "massless" taxonomy.
         if (cfg%ocean%ice_ic%h_snow > 0.0_wp .and. cfg%ocean%ice_ic%h_ice <= 0.0_wp) then
            call logger%error("&ocean_ice_ic_nml h_snow > 0 requires h_ice > 0 "// &
                              "(orphan snow with no ice is silently absorbed as "// &
                              "'massless' by the ITD restore)")
            has_error = .true.
         end if
         ! V8: belt-and-braces on top of nml_enum allowed= — catches an
         ! unrecognised conc_config string even if the schema layer is
         ! ever bypassed.
         if (ice_ic_parse_conc_config(cfg%ocean%ice_ic%conc_config) == ICE_IC_CONC_INVALID) then
            call logger%error("&ocean_ice_ic_nml conc_config='"// &
                              trim(cfg%ocean%ice_ic%conc_config)// &
                              "' is not recognised (allowed: zero, uniform, latitudes)")
            has_error = .true.
         end if
         ! V9: the IC is skipped on resume (a restart REPLACES it, never
         ! merges) — warn, don't abort, so the legitimate "same nml, cold
         ! then warm" workflow still works.
         if (len_trim(cfg%restart_file) > 0) then
            call logger%warning("&ocean_ice_ic_nml conc_config /= 'zero' with "// &
                                "restart_file set: the ice IC is SKIPPED on a warm "// &
                                "restart (the restart replaces it, it does not merge "// &
                                "with it) — this namelist's ice IC knobs have no effect "// &
                                "on this run")
         end if
      end if
      ! ---- Sea-ice PR 26: snowfall source-term envelope ----
      ! `snowfall > 0` is a request for `ice_atm_forcing_restoring` +
      ! `ice_thermo_driver_step`/`ice_snowfall_ocean_share` to fire every
      ! thermo window; both live inside the `enable_thermodynamics .and.
      ! is_thermo_step()` ice block in the driver, so a config with the
      ! ice slot or thermodynamics off would leave `snowfall` a wordless
      ! no-op — fail loud instead (house rule, mirrors the transport
      ! envelope above).
      if (cfg%ocean%ice%snowfall > 0.0_wp) then
         if (.not. cfg%ocean%ice%enable) then
            call logger%error("&ocean_ice_nml snowfall > 0 requires "// &
                              "enable=.true. (the ice slot must be live)")
            has_error = .true.
         end if
         if (.not. cfg%ocean%thermo%enable_thermodynamics) then
            call logger%error("&ocean_ice_nml snowfall > 0 requires "// &
                              "&ocean_thermo_nml enable_thermodynamics=.true. "// &
                              "(the atmospheric-forcing fill and the column "// &
                              "snow add both fire only on the thermo cadence)")
            has_error = .true.
         end if
      end if

      ! ---- Sea-ice PR 62: a_face_stress requires dynamics ----
      ! `a_face_stress` only reaches the EVP momentum solve — with
      ! `dynamics=.false.` it would be a wordless no-op.  Fail loud instead
      ! (house rule).  Deliberately OUTSIDE the `if (dynamics)` envelope
      ! above: it must fire precisely when `dynamics` is false.
      if (cfg%ocean%ice%a_face_stress .and. .not. cfg%ocean%ice%dynamics) then
         call logger%error("&ocean_ice_nml a_face_stress=.true. requires dynamics=.true. "// &
                           "(the knob only reaches the EVP momentum solve)")
         has_error = .true.
      end if

      ! ---- Sea-ice PR 27: Archimedes snow-ice flooding envelope ----
      ! `snow_ice=.true.` is a request for `ice_snow_ice_flood` to fire
      ! inside `ice_column_step`'s resize stage every thermo window; that
      ! call lives inside the `enable_thermodynamics .and. is_thermo_
      ! step()` ice block in the driver, so a config with the ice slot or
      ! thermodynamics off would leave `snow_ice` a wordless no-op — fail
      ! loud instead (house rule, mirrors the transport/snowfall envelopes
      ! above).
      if (cfg%ocean%ice%snow_ice) then
         if (.not. cfg%ocean%ice%enable) then
            call logger%error("&ocean_ice_nml snow_ice=.true. requires "// &
                              "enable=.true. (the ice slot must be live)")
            has_error = .true.
         end if
         if (.not. cfg%ocean%thermo%enable_thermodynamics) then
            call logger%error("&ocean_ice_nml snow_ice=.true. requires "// &
                              "&ocean_thermo_nml enable_thermodynamics=.true. "// &
                              "(the column runs only on the thermo cadence)")
            has_error = .true.
         end if
      end if

      ! ---- BT wide-halo march-in (bt_halo > 0) exclusions ----
      if (cfg%ocean%bt%bt_halo > 0) then
         if (cfg%ocean%wetdry%enable) then
            call logger%error("&ocean_bt_nml bt_halo > 0 is mutually exclusive "// &
                              "with &ocean_wetdry_nml enable (wd arrays not widened in v1)")
            has_error = .true.
         end if
         if (cfg%ocean%bt%use_cont_type) then
            call logger%error("&ocean_bt_nml bt_halo > 0 is mutually exclusive "// &
                              "with use_cont_type (BTCL arrays not widened in v1)")
            has_error = .true.
         end if
         if (cfg%ocean%bt%upstream_h_face) then
            call logger%error("&ocean_bt_nml bt_halo > 0 is mutually exclusive "// &
                              "with upstream_h_face (upstream h-face arrays not widened in v1)")
            has_error = .true.
         end if
         if (cfg%ocean%tides%enable) then
            call logger%error("&ocean_bt_nml bt_halo > 0 is mutually exclusive "// &
                              "with &ocean_tides_nml enable (wide eta_forcing copy deferred)")
            has_error = .true.
         end if
         if (cfg%ocean%psurf%enable) then
            call logger%error("&ocean_bt_nml bt_halo > 0 is mutually exclusive "// &
                              "with &ocean_psurf_nml enable (wide eta_forcing copy deferred)")
            has_error = .true.
         end if
         if (cfg%ocean%porous%enable) then
            ! `bt_wide` carries its OWN `metrics_w`, re-filled from the
            ! grid formula on the widened grid.  Nothing fills its porous
            ! statistics, so `bt_wide_substep` would hand the fast loop the
            ! UN-narrowed `metrics_w%dy_cu` / `dx_cv` — silently reverting
            ! the porous-aware barotropic solve and giving an answer that
            ! is neither the porous one nor the baseline.  Fail loud until
            ! the wide shadow carries `dy_cu_bt` / `dx_cv_bt` too.
            call logger%error("&ocean_bt_nml bt_halo > 0 is mutually exclusive "// &
                              "with &ocean_porous_nml enable (the wide-halo "// &
                              "metrics shadow carries no porous statistics, so the "// &
                              "BT solve would silently transport on un-narrowed widths)")
            has_error = .true.
         end if
         if (cfg%ocean%cavity_dyn%enable) then
            ! Same failure mode as porous, one level up: `metrics_w` is
            ! re-filled from the grid formula and carries no `z_draft`, so
            ! the wide fast loop would take the BED as its reference depth
            ! and solve a column that is twice as deep as the cavity's.
            ! (The cavity block above refuses this from its own side too —
            ! the knob that is "wrong" depends on which one the user meant.)
            call logger%error("&ocean_bt_nml bt_halo > 0 is mutually exclusive "// &
                              "with &ocean_cavity_dyn_nml enable (the wide-halo "// &
                              "metrics shadow carries no ice draft, so the BT solve "// &
                              "would reference the bed instead of the ice base)")
            has_error = .true.
         end if
         if (trim(cfg%ocean%grid%grid_config) == "supergrid" .or. &
             trim(cfg%ocean%grid%grid_config) == "tripolar") then
            call logger%error("&ocean_bt_nml bt_halo > 0 is mutually exclusive "// &
                              "with grid_config='"//trim(cfg%ocean%grid%grid_config)// &
                              "' (file-based metrics cannot be re-filled on a wide grid; "// &
                              "cartesian/spherical formula fills supported)")
            has_error = .true.
         end if
         if (ocean_bt_rem_from_visc_rem_on(cfg)) then
            ! Same check as above, from the bt_halo side — belt-and-
            ! braces so the error fires regardless of which knob a reader
            ! finds first in the namelist.  PR-3 (D1): reads through the
            ! helper, so visc_rem_chain = .true. is caught too.
            call logger%error("&ocean_bt_nml bt_halo > 0 is mutually exclusive "// &
                              "with bt_rem_from_visc_rem=.true. (or "// &
                              "visc_rem_chain=.true.; the wide-halo BT clone carries "// &
                              "no av_rem/visc_rem ghost-width statistics, like porous)")
            has_error = .true.
         end if
      end if
      ! ---- Porous barriers (&ocean_porous_nml) exclusions ----
      if (cfg%ocean%porous%enable) then
         if (cfg%ocean%wetdry%enable) then
            ! A drying column drives every layer's face thickness below
            ! H_VANISHED, where the open fractions all go to zero and the
            ! barotropic width goes with them.  That is self-consistent but
            ! wholly untested against the wet/dry outflow limiter, and the
            ! two schemes each own part of the same "can this face carry
            ! transport" decision.  Refuse the combination rather than ship
            ! an unvalidated interaction.
            call logger%error("&ocean_porous_nml enable is mutually exclusive with "// &
                              "&ocean_wetdry_nml enable (the vanishing-column "// &
                              "interaction between the open fractions and the "// &
                              "wet/dry outflow limiter is unvalidated)")
            has_error = .true.
         end if
      end if

      ! ---- Ideal-age tracer knob sanity (PR-7, fail-quiet class) ----
      ! Neither combination is unsafe, so both are warnings, not aborts —
      ! and the default (0/0) must not trip either guard.
      if (cfg%ocean%tracers%ideal_age_sfc_growth_rate /= 0.0_wp .and. &
          cfg%ocean%tracers%ideal_age_young_val == 0.0_wp) then
         call logger%warning("&ocean_tracers_nml ideal_age_sfc_growth_rate is nonzero but "// &
                             "ideal_age_young_val = 0: young_val*exp(...) is identically 0, "// &
                             "so the growth-rate knob is silently inert (MOM6 seeds its "// &
                             "vintage tracer with young_val = 1e-20 for exactly this reason)")
      end if
      if (.not. cfg%ocean%tracers%enable_ideal_age .and. &
          (cfg%ocean%tracers%ideal_age_young_val /= 0.0_wp .or. &
           cfg%ocean%tracers%ideal_age_sfc_growth_rate /= 0.0_wp)) then
         call logger%warning("&ocean_tracers_nml ideal_age_young_val / "// &
                             "ideal_age_sfc_growth_rate are set but enable_ideal_age = "// &
                             ".false.: no age tracer is registered, so both knobs are "// &
                             "silently inert")
      end if

      ! ---- Surface-flux component set (PR-12) ----
      ! The component set only feeds the thermo path (the assembler
      ! derives Q_heat/Q_salt, which apply_tracers stamps onto the
      ! tracer hTr); with thermo off the whole thing is inert. Fail loud
      ! rather than let a user carry the (real, gated-off) device memory
      ! for nothing.
      if (forcing_components_need_thermo(cfg%ocean%forcing%enable_components, &
                                         cfg%ocean%thermo%enable_thermodynamics)) then
         call logger%error("&ocean_forcing_nml enable_components=.true. requires "// &
                           "&ocean_thermo_nml enable_thermodynamics=.true. "// &
                           "(the component set feeds the tracer path; it is inert "// &
                           "under enable_thermodynamics=.false.)")
         has_error = .true.
      end if

      ! ---- &ocean_dataovr_nml (PR-15 file-backed surface forcing) ----
      !
      ! Validated HERE, not at registration, on the "a broken config must
      ! not run at all" principle: `ocean_data_forcing_configure` runs
      ! deep in the driver's setup phase, AFTER the grid, bathymetry,
      ! initial condition and a dozen slots are built.  Catching a
      ! namelist typo there means the user pays for all of that first.
      ! Everything below is decidable from `cfg` alone, so it costs
      ! nothing to decide it before any work happens.
      !
      ! The fail-loud guards inside `rdb_ocean_data_forcing` STAY: they
      ! are the gate for a programmatic caller (a future Python driver
      ! registers fields directly and never parses a namelist), and this
      ! block is unreachable for that path.
      if (cfg%ocean%dataovr%enable) then
#ifdef RDB_NO_NETCDF
         call logger%error("&ocean_dataovr_nml enable=.true. requires a NetCDF build "// &
                           "(RDB_ENABLE_NETCDF=ON); the reader is compiled out here")
         has_error = .true.
#endif
         ! D1: a named file with no variable name.  No guessed fallbacks
         ! for forcing — an unintended variable is worse than a stop.
         if (.not. dataovr_entry_is_valid(cfg%ocean%dataovr%tau_x)) then
            call logger%error("&ocean_dataovr_nml tau_x_file is set but tau_x_var is blank")
            has_error = .true.
         end if
         if (.not. dataovr_entry_is_valid(cfg%ocean%dataovr%tau_y)) then
            call logger%error("&ocean_dataovr_nml tau_y_file is set but tau_y_var is blank")
            has_error = .true.
         end if
         if (.not. dataovr_entry_is_valid(cfg%ocean%dataovr%heat)) then
            call logger%error("&ocean_dataovr_nml heat_file is set but heat_var is blank")
            has_error = .true.
         end if
         if (.not. dataovr_entry_is_valid(cfg%ocean%dataovr%evap)) then
            call logger%error("&ocean_dataovr_nml evap_file is set but evap_var is blank")
            has_error = .true.
         end if
         if (.not. dataovr_entry_is_valid(cfg%ocean%dataovr%lprec)) then
            call logger%error("&ocean_dataovr_nml lprec_file is set but lprec_var is blank")
            has_error = .true.
         end if
         if (.not. dataovr_entry_is_valid(cfg%ocean%dataovr%salt)) then
            call logger%error("&ocean_dataovr_nml salt_file is set but salt_var is blank")
            has_error = .true.
         end if

         ! D2: cyclic climatology with no period is undecidable, not
         ! defaultable — there is no sane fallback for "one year".
         if (.not. dataovr_time_is_valid(cfg%ocean%dataovr)) then
            call logger%error("&ocean_dataovr_nml time_mode='cyclic' requires cycle_period > 0")
            has_error = .true.
         end if

         ! D3: the freshwater tags exist ONLY in the surface-flux
         ! component set; without it there is no slot to write into.
         if (dataovr_freshwater_needs_components(cfg%ocean%dataovr, &
                                                 cfg%ocean%forcing%enable_components)) then
            call logger%error("&ocean_dataovr_nml evap/lprec file forcing requires "// &
                              "&ocean_forcing_nml enable_components=.true. "// &
                              "(there is no non-component freshwater slot to write into)")
            has_error = .true.
         end if

         ! D4: a named file that does not exist.  A typo'd path is the
         ! single most common way to get this group wrong, and without
         ! this check it surfaces only once registration opens the file
         ! — deep in setup, as a bare "NetCDF operation failed" that
         ! does not even name the path.  `inquire` needs no NetCDF, so
         ! this works in every build.  It is a genuine
         ! time-of-check/time-of-use race (the file could vanish before
         ! registration), which is fine: registration still fails loud,
         ! this only moves the COMMON case earlier and makes it legible.
         call check_dataovr_file(cfg%ocean%dataovr%tau_x, "tau_x", has_error)
         call check_dataovr_file(cfg%ocean%dataovr%tau_y, "tau_y", has_error)
         call check_dataovr_file(cfg%ocean%dataovr%heat, "heat", has_error)
         call check_dataovr_file(cfg%ocean%dataovr%evap, "evap", has_error)
         call check_dataovr_file(cfg%ocean%dataovr%lprec, "lprec", has_error)
         call check_dataovr_file(cfg%ocean%dataovr%salt, "salt", has_error)

         ! D5: enabled but nothing driven is a wordless no-op — the
         ! house "silent no-op => fail loud" rule (cf. V2/V3 above).
         if (.not. dataovr_any_tag_set(cfg%ocean%dataovr)) then
            call logger%error("&ocean_dataovr_nml enable=.true. but no <tag>_file is set — "// &
                              "either name a forcing file or set enable=.false.")
            has_error = .true.
         end if
      end if

      if (has_error) then
         ! NOTE: this is a terminal ROLLUP over many independent checks above
         ! (most of which log their own specific reason via `logger%error`
         ! but do not individually push to the error ring — instrumenting
         ! all ~160 of them is out of scope for this pass). This message is
         ! therefore GENERIC by construction; a caller wanting the specific
         ! reason should also look at ring index 1 (the most recently
         ! instrumented check, if any fired) rather than only index 0.
         call fail("Configuration validation failed — see errors above", ierr, &
                   OCEAN_STATUS_ERR_CONFIG_VALIDATE)
         return
      end if
      if (present(ierr)) ierr = OCEAN_STATUS_OK

   end subroutine validate_config