diff --git a/scipost_django/finances/views.py b/scipost_django/finances/views.py
index 68504106cae576159b45102c012242dce2dd99fc..1774b925080766f627f4280f8121d0fcb06ada11 100644
--- a/scipost_django/finances/views.py
+++ b/scipost_django/finances/views.py
@@ -40,7 +40,7 @@ from comments.utils import validate_file_extention
 from journals.models import Journal, Publication
 from organizations.models import Organization
 from scipost.mixins import PermissionsMixin
-from scipost.views import HTMXPermissionsDenied, HTMXResponse
+from scipost.permissions import HTMXPermissionsDenied, HTMXResponse
 
 
 def publishing_years():
diff --git a/scipost_django/production/permissions.py b/scipost_django/production/permissions.py
index 909d5b9a5c4dbe4b40f91c9e5cbbd50c219d94a8..841dd945192969a81446bd66df6b8c987ec5dab2 100644
--- a/scipost_django/production/permissions.py
+++ b/scipost_django/production/permissions.py
@@ -3,9 +3,6 @@ __license__ = "AGPL v3"
 
 
 from django.contrib.auth.decorators import user_passes_test
-from scipost.views import HTMXPermissionsDenied
-from functools import wraps
-from django.contrib import messages
 
 
 def is_production_user():
@@ -18,27 +15,3 @@ def is_production_user():
         return False
 
     return user_passes_test(test)
-
-
-def permission_required_htmx(
-    perm,
-    message="You do not have the required permissions.",
-    **message_kwargs,
-):
-    def decorator(view_func):
-        @wraps(view_func)
-        def _wrapped_view(request, *args, **kwargs):
-            if isinstance(perm, str):
-                perms = (perm,)
-            else:
-                perms = perm
-
-            if request.user.has_perms(perms):
-                return view_func(request, *args, **kwargs)
-            else:
-                messages.error(request, message)
-                return HTMXPermissionsDenied(message, **message_kwargs)
-
-        return _wrapped_view
-
-    return decorator
diff --git a/scipost_django/production/views.py b/scipost_django/production/views.py
index 679b5770823d319f0b638c455cf677a6eed8fa03..f95e42c53d774d8a4a245602a405609f7c38ef8b 100644
--- a/scipost_django/production/views.py
+++ b/scipost_django/production/views.py
@@ -21,7 +21,11 @@ from guardian.shortcuts import assign_perm, remove_perm
 
 from finances.forms import WorkLogForm
 from mails.views import MailEditorSubviewHTMX
-from scipost.views import HTMXPermissionsDenied, HTMXResponse
+from scipost.permissions import (
+    HTMXPermissionsDenied,
+    HTMXResponse,
+    permission_required_htmx,
+)
 
 from . import constants
 from .models import (
@@ -44,7 +48,7 @@ from .forms import (
     ProofsDecisionForm,
     AssignInvitationsOfficerForm,
 )
-from .permissions import is_production_user, permission_required_htmx
+from .permissions import is_production_user
 from .utils import proofs_slug_to_id, ProductionUtils
 
 
diff --git a/scipost_django/profiles/templates/profiles/profile_list.html b/scipost_django/profiles/templates/profiles/profile_list.html
index 16c8c1907eb13ac46ed8e19aa82d4a048a0b433f..0a066f0113debf0390aa7d24ae86c128f90e528c 100644
--- a/scipost_django/profiles/templates/profiles/profile_list.html
+++ b/scipost_django/profiles/templates/profiles/profile_list.html
@@ -8,178 +8,372 @@
 
 
 {% block breadcrumb_items %}
-  {{ block.super }}
-  <span class="breadcrumb-item">Profiles</span>
+    {{ block.super }}
+    <span class="breadcrumb-item">Profiles</span>
 {% endblock %}
 
 
-{% block meta_description %}{{ block.super }} Profiles List{% endblock meta_description %}
-{% block pagetitle %}: Profiles{% endblock pagetitle %}
+{% block meta_description %}
+    {{ block.super }} Profiles List
+{% endblock meta_description %}
+{% block pagetitle %}
+    : Profiles
+{% endblock pagetitle %}
 
 {% block content %}
 
-  {% is_ed_admin request.user as is_ed_admin %}
-  {% is_scipost_admin request.user as is_scipost_admin %}
-
-  <div class="row">
-    <div class="col-12">
-      <h4>Profiles-related Actions:</h4>
-      <ul>
-	{% if is_scipost_admin or is_ed_admin %}
-	  {% if nr_contributors_w_duplicate_names > 0 %}
-	    <li><span class="text-warning">{% include 'bi/exclamation-circle-fill.html' %}</span><a href="{% url 'scipost:contributor_duplicates' %}?kind=names">Handle Contributors with duplicate names ({{ nr_contributors_w_duplicate_names }} to handle)</a></li>
-	  {% else %}
-	    <li><span class="text-success">{% include 'bi/check-circle-fill.html' %}</span> No name-duplicate Contributors found</li>
-	  {% endif %}
-	  {% if nr_contributors_w_duplicate_emails > 0 %}
-	    <li><a href="{% url 'scipost:contributor_duplicates' %}?kind=emails">Handle Contributors with duplicate emails ({{ nr_contributors_w_duplicate_emails }} to handle)</a></li>
-	  {% else %}
-	    <li><span class="text-success">{% include 'bi/check-circle-fill.html' %}</span> No email-duplicate Contributors found</li>
-	  {% endif %}
-	  {% if next_contributor_wo_profile %}
-	    <li><span class="text-warning">{% include 'bi/exclamation-circle-fill.html' %}</span> Create a Profile for <a href="{% url 'profiles:profile_create' from_type='contributor' pk=next_contributor_wo_profile.id %}">the next</a> Contributor without one ({{ nr_contributors_wo_profile }} to handle)</li>
-	  {% else %}
-	    <li><span class="text-success">{% include 'bi/check-circle-fill.html' %}</span> All registered Contributors have a Profile</li>
-	  {% endif %}
-	  {% if nr_potential_duplicate_profiles > 0 %}
-	    <li><span class="text-warning">{% include 'bi/exclamation-circle-fill.html' %}</span> <a href="{% url 'profiles:duplicates' %}">Check for duplicate Profiles ({{ nr_potential_duplicate_profiles }} to handle)</a></li>
-	  {% else %}
-	    <li><span class="text-success">{% include 'bi/check-circle-fill.html' %}</span> No potential duplicate Profiles detected</li>
-	  {% endif %}
-	  {% if next_reginv_wo_profile %}
-	    <li><span class="text-warning">{% include 'bi/exclamation-circle-fill.html' %}</span> Create a Profile for <a href="{% url 'profiles:profile_create' from_type='registrationinvitation' pk=next_reginv_wo_profile.id %}">the next</a> Registration Invitation without one ({{ nr_reginv_wo_profile }} to handle)</li>
-	  {% else %}
-	    <li><span class="text-success">{% include 'bi/check-circle-fill.html' %}</span> All Registration Invitations have a Profile</li>
-	  {% endif %}
-	  {% if next_refinv_wo_profile %}
-	    <li><span class="text-warning">{% include 'bi/exclamation-circle-fill.html' %}</span> Create a Profile for <a href="{% url 'profiles:profile_create' from_type='refereeinvitation' pk=next_refinv_wo_profile.id %}">the next</a> Referee Invitation without one ({{ nr_refinv_wo_profile }} to handle)</li>
-	  {% else %}
-	    <li><span class="text-success">{% include 'bi/check-circle-fill.html' %}</span> All Referee Invitations have a Profile</li>
-	  {% endif %}
-	{% endif %}
-	<li><a href="{% url 'profiles:profile_create' %}">Add a Profile</a></li>
-      </ul>
-    </div>
-  </div>
-
-  <div class="row">
-    <div class="col-12">
-      <table class="table table-bordered table-secondary">
-	<thead class="table-dark">
-	  <tr>
-	    <th><h3 class="mb-0">Branch</h3></th>
-	    <th><h3 class="mb-0">Fields</h3></th>
-	  </tr>
-	</thead>
-	<tbody>
-	  {% for branch in branches %}
-	    <tr>
-	      <td class="align-middle">
-		<small>{{ branch.name }}</small>
-	      </td>
-	      <td>
-		<ul class="list-inline m-0">
-		  {% for acad_field in branch.academic_fields.all %}
-		    <li class="list-inline-item">
-		      {% if acad_field.profiles.all|length > 0 %}
-			<div class="dropdown">
-			  <button class="btn btn-sm btn-primary dropdown-toggle" type="button" id="dropdownMenuButton{{ acad_field.slug }}" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><small>{{ acad_field }}</small></button>
-			  <div class="dropdown-menu" aria-labelledby="dropdownMenuButton{{ acad_field.slug }}">
-			    <a class="dropdown-item" href="{% add_get_parameters field=acad_field.slug specialty='' %}">View all in {{ acad_field }}</a>
-			    {% for specialty in acad_field.specialties.all %}
-			      <a class="dropdown-item" href="{% add_get_parameters field=acad_field.slug specialty=specialty.slug %}">{{ specialty }}</a>
-			    {% endfor %}
-			  </div>
-			</div>
-		      {% else %}
-			<button type="button" class="btn btn-sm btn-outline-secondary m-1"><small><em>{{ acad_field.name }}</em></small></button>
-		      {% endif %}
-		    </li>
-		  {% endfor %}
-	      </td>
-	    </tr>
-	  {% endfor %}
-	</tbody>
-      </table>
-
-
-      <h4>Specialize the list by selecting from the table above, or:</h4>
-      <ul>
-	<li>
-	  <ul class="list-inline">
-	    <li class="list-inline-item">
-	      <a href="{% url 'profiles:profiles' %}">View all</a>
-	    </li>
-	  </ul>
-	</li>
-	<li>View only Profiles <a href="{% add_get_parameters contributor=True %}">with</a> or <a href="{% add_get_parameters contributor=False %}">without</a> an associated Contributor</li>
-	<li>
-	  <ul class="list-inline">
-	    <li class="list-inline-item">Last name contains:</li>
-	    <li class="list-inline-item">
-	      <form action="" method="get">{{ searchform }}
-		{% if request.GET.field %}
-		  <input type="hidden" name="field" value="{{ request.GET.field }}">
-		  {% if request.GET.specialty %}
-		    <input type="hidden" name="specialty" value="{{ request.GET.specialty }}">
-		  {% endif %}
-		{% endif %}
-		{% if request.GET.contributor %}
-		  <input type="hidden" name="contributor" value="{{ request.GET.contributor }}">
-		{% endif %}
-	    </li>
-	    <li class="list-inline-item"><input class="btn btn-outline-secondary" type="submit" value="Search"></form>
-	    </li>
-	  </ul>
-	</li>
-      </ul>
-    </div>
-  </div>
-
-  <div class="row">
-    <div class="col-12">
-      <h3>Profiles {% if request.GET.text %}with last name starting with {{ request.GET.text }}{% endif %} {% if request.GET.field %}in {{ request.GET.field }}{% if request.GET.specialty %}, {{ request.GET.specialty }}{% endif %}{% endif %} ({% if request.GET.contributor == "True" %}registered Contributors{% elif request.GET.contributor == "False" %}unregistered as Contributors{% else %}all registered/unregistered{% endif %}): {{ page_obj.paginator.count }} found</h3>
-      <br/>
-
-      <table class="table table-hover mb-5">
-	<thead class="table-light">
-	  <tr>
-	    <th>Name</th>
-	    <th>Academic field</th>
-	    <th>Specialties</th>
-	    <th>Contributor?</th>
-	  </tr>
-	</thead>
-	<tbody>
-	  {% for profile in object_list %}
-	    <tr class="table-row" data-href="{% url 'profiles:profile_detail' pk=profile.id %}" target="_blank" style="cursor: pointer;">
-	      <td>{{ profile }}</td>
-	      <td>{{ profile.acad_field }}</td>
-	      <td>
-		{% for specialty in profile.specialties.all %}
-		  <div class="single d-inline" data-specialty="{{ specialty.code }}" data-bs-toggle="tooltip" data-bs-placement="bottom" title="{{ specialty }}">{{ specialty.code }}</div>
-		{% endfor %}
-	      </td>
-	      <td>{% if profile.has_active_contributor %}<span class="text-success">{% include 'bi/check-circle-fill.html' %}</span>{% else %}<span class="text-danger">{% include 'bi/x-circle-fill.html' %}</span>{% endif %}</td>
-	    </tr>
-	  {% empty %}
-	    <tr>
-	      <td colspan="4">No Profiles found</td>
-	    </tr>
-	  {% endfor %}
-	</tbody>
-      </table>
-
-      {% if is_paginated %}
-	<div class="col-12">
-	  {% include '_pagination.html' with page_obj=page_obj %}
-	</div>
-      {% endif %}
+    {% is_ed_admin request.user as is_ed_admin %}
+    {% is_scipost_admin request.user as is_scipost_admin %}
 
+    <div class="row">
+        <div class="col-12">
+            <h4>Profiles-related Actions:</h4>
+            <ul>
+	
+                {% if is_scipost_admin or is_ed_admin %}
+ 
+                    {% if nr_contributors_w_duplicate_names > 0 %}
+ 
+                        <li>
+                            <span class="text-warning">{% include 'bi/exclamation-circle-fill.html' %}</span><a href="{% url 'scipost:contributor_duplicates' group_by="names" %}">Handle Contributors with duplicate names ({{ nr_contributors_w_duplicate_names }} to handle)</a>
+                        </li>
+ 
+                    {% else %}
+ 
+                        <li>
+                            <span class="text-success">{% include 'bi/check-circle-fill.html' %}</span> No name-duplicate Contributors found
+                        </li>
+ 
+                    {% endif %}
+ 
+                    {% if nr_contributors_w_duplicate_emails > 0 %}
+ 
+                        <li>
+                            <a href="{% url 'scipost:contributor_duplicates' group_by="emails" %}">Handle Contributors with duplicate emails ({{ nr_contributors_w_duplicate_emails }} to handle)</a>
+                        </li>
+ 
+                    {% else %}
+ 
+                        <li>
+                            <span class="text-success">{% include 'bi/check-circle-fill.html' %}</span> No email-duplicate Contributors found
+                        </li>
+ 
+                    {% endif %}
+ 
+                    {% if next_contributor_wo_profile %}
+ 
+                        <li>
+                            <span class="text-warning">{% include 'bi/exclamation-circle-fill.html' %}</span> Create a Profile for <a href="{% url 'profiles:profile_create' from_type='contributor' pk=next_contributor_wo_profile.id %}">the next</a> Contributor without one ({{ nr_contributors_wo_profile }} to handle)
+                        </li>
+ 
+                    {% else %}
+ 
+                        <li>
+                            <span class="text-success">{% include 'bi/check-circle-fill.html' %}</span> All registered Contributors have a Profile
+                        </li>
+ 
+                    {% endif %}
+ 
+                    {% if nr_potential_duplicate_profiles > 0 %}
+ 
+                        <li>
+                            <span class="text-warning">{% include 'bi/exclamation-circle-fill.html' %}</span> <a href="{% url 'profiles:duplicates' %}">Check for duplicate Profiles ({{ nr_potential_duplicate_profiles }} to handle)</a>
+                        </li>
+ 
+                    {% else %}
+ 
+                        <li>
+                            <span class="text-success">{% include 'bi/check-circle-fill.html' %}</span> No potential duplicate Profiles detected
+                        </li>
+ 
+                    {% endif %}
+ 
+                    {% if next_reginv_wo_profile %}
+ 
+                        <li>
+                            <span class="text-warning">{% include 'bi/exclamation-circle-fill.html' %}</span> Create a Profile for <a href="{% url 'profiles:profile_create' from_type='registrationinvitation' pk=next_reginv_wo_profile.id %}">the next</a> Registration Invitation without one ({{ nr_reginv_wo_profile }} to handle)
+                        </li>
+ 
+                    {% else %}
+ 
+                        <li>
+                            <span class="text-success">{% include 'bi/check-circle-fill.html' %}</span> All Registration Invitations have a Profile
+                        </li>
+ 
+                    {% endif %}
+ 
+                    {% if next_refinv_wo_profile %}
+ 
+                        <li>
+                            <span class="text-warning">{% include 'bi/exclamation-circle-fill.html' %}</span> Create a Profile for <a href="{% url 'profiles:profile_create' from_type='refereeinvitation' pk=next_refinv_wo_profile.id %}">the next</a> Referee Invitation without one ({{ nr_refinv_wo_profile }} to handle)
+                        </li>
+ 
+                    {% else %}
+ 
+                        <li>
+                            <span class="text-success">{% include 'bi/check-circle-fill.html' %}</span> All Referee Invitations have a Profile
+                        </li>
+ 
+                    {% endif %}
+	
+                {% endif %}
+	
+                <li>
+                    <a href="{% url 'profiles:profile_create' %}">Add a Profile</a>
+                </li>
+            </ul>
+        </div>
     </div>
-  </div>
-{% endblock content %}
 
-{% block footer_script %}
-  <script src="{% static 'scipost/table-row.js' %}"></script>
-{% endblock footer_script %}
+    <div class="row">
+        <div class="col-12">
+            <table class="table table-bordered table-secondary">
+	
+                <thead class="table-dark">
+ 
+                    <tr>
+ 
+                        <th>
+                            <h3 class="mb-0">Branch</h3>
+                        </th>
+ 
+                        <th>
+                            <h3 class="mb-0">Fields</h3>
+                        </th>
+ 
+                    </tr>
+	
+                </thead>
+	
+                <tbody>
+ 
+                    {% for branch in branches %}
+ 
+                        <tr>
+ 
+                            <td class="align-middle">
+                                <small>{{ branch.name }}</small>
+ 
+                            </td>
+ 
+                            <td>
+	
+                                <ul class="list-inline m-0">
+ 
+                                    {% for acad_field in branch.academic_fields.all %}
+ 
+                                        <li class="list-inline-item">
+ 
+                                            {% if acad_field.profiles.all|length > 0 %}
+	
+                                                <div class="dropdown">
+ 
+                                                    <button class="btn btn-sm btn-primary dropdown-toggle"
+                                                            type="button"
+                                                            id="dropdownMenuButton{{ acad_field.slug }}"
+                                                            data-bs-toggle="dropdown"
+                                                            aria-haspopup="true"
+                                                            aria-expanded="false">
+                                                        <small>{{ acad_field }}</small>
+                                                    </button>
+ 
+                                                    <div class="dropdown-menu"
+                                                         aria-labelledby="dropdownMenuButton{{ acad_field.slug }}">
+                                                        <a class="dropdown-item"
+                                                           href="{% add_get_parameters field=acad_field.slug specialty='' %}">View all in {{ acad_field }}</a>
+ 
+                                                        {% for specialty in acad_field.specialties.all %}
+                                                            <a class="dropdown-item"
+                                                               href="{% add_get_parameters field=acad_field.slug specialty=specialty.slug %}">{{ specialty }}</a>
+ 
+                                                        {% endfor %}
+ 
+                                                    </div>
+	
+                                                </div>
+ 
+                                            {% else %}
+	
+                                                <button type="button" class="btn btn-sm btn-outline-secondary m-1">
+                                                    <small><em>{{ acad_field.name }}</em></small>
+                                                </button>
+ 
+                                            {% endif %}
+ 
+                                        </li>
+ 
+                                    {% endfor %}
+ 
+                                </td>
+ 
+                            </tr>
+ 
+                        {% endfor %}
+	
+                    </tbody>
+                </table>
+
+
+                <h4>Specialize the list by selecting from the table above, or:</h4>
+                <ul>
+	
+                    <li>
+ 
+                        <ul class="list-inline">
+ 
+                            <li class="list-inline-item">
+                                <a href="{% url 'profiles:profiles' %}">View all</a>
+ 
+                            </li>
+ 
+                        </ul>
+	
+                    </li>
+	
+                    <li>
+                        View only Profiles <a href="{% add_get_parameters contributor=True %}">with</a> or <a href="{% add_get_parameters contributor=False %}">without</a> an associated Contributor
+                    </li>
+	
+                    <li>
+ 
+                        <ul class="list-inline">
+ 
+                            <li class="list-inline-item">Last name contains:</li>
+ 
+                            <li class="list-inline-item">
+ 
+                                <form action="" method="get">
+                                    {{ searchform }}
+	
+                                    {% if request.GET.field %}
+ 
+                                        <input type="hidden" name="field" value="{{ request.GET.field }}">
+ 
+                                        {% if request.GET.specialty %}<input type="hidden" name="specialty" value="{{ request.GET.specialty }}">{% endif %}
+	
+                                    {% endif %}
+	
+                                    {% if request.GET.contributor %}
+ 
+                                        <input type="hidden"
+                                               name="contributor"
+                                               value="{{ request.GET.contributor }}">
+	
+                                    {% endif %}
+ 
+                                </li>
+ 
+                                <li class="list-inline-item">
+                                    <input class="btn btn-outline-secondary" type="submit" value="Search">
+                                </form>
+ 
+                            </li>
+ 
+                        </ul>
+	
+                    </li>
+                </ul>
+            </div>
+        </div>
+
+        <div class="row">
+            <div class="col-12">
+                <h3>
+                    Profiles
+                    {% if request.GET.text %}with last name starting with {{ request.GET.text }}{% endif %}
+ 
+                    {% if request.GET.field %}
+                        in {{ request.GET.field }}
+                        {% if request.GET.specialty %}, {{ request.GET.specialty }}{% endif %}
+                    {% endif %}
+                    (
+                    {% if request.GET.contributor == "True" %}
+                        registered Contributors
+                    {% elif request.GET.contributor == "False" %}
+                        unregistered as Contributors
+                    {% else %}
+                        all registered/unregistered
+                    {% endif %}
+                    ): {{ page_obj.paginator.count }} found
+                </h3>
+                <br />
+
+                <table class="table table-hover mb-5">
+	
+                    <thead class="table-light">
+ 
+                        <tr>
+ 
+                            <th>Name</th>
+ 
+                            <th>Academic field</th>
+ 
+                            <th>Specialties</th>
+ 
+                            <th>Contributor?</th>
+ 
+                        </tr>
+	
+                    </thead>
+	
+                    <tbody>
+ 
+                        {% for profile in object_list %}
+ 
+                            <tr class="table-row"
+                                data-href="{% url 'profiles:profile_detail' pk=profile.id %}"
+                                target="_blank"
+                                style="cursor: pointer">
+ 
+                                <td>{{ profile }}</td>
+ 
+                                <td>{{ profile.acad_field }}</td>
+ 
+                                <td>
+	
+                                    {% for specialty in profile.specialties.all %}
+ 
+                                        <div class="single d-inline"
+                                             data-specialty="{{ specialty.code }}"
+                                             data-bs-toggle="tooltip"
+                                             data-bs-placement="bottom"
+                                             title="{{ specialty }}">{{ specialty.code }}</div>
+	
+                                    {% endfor %}
+ 
+                                </td>
+ 
+                                <td>
+                                    {% if profile.has_active_contributor %}
+                                        <span class="text-success">{% include 'bi/check-circle-fill.html' %}</span>
+                                    {% else %}
+                                        <span class="text-danger">{% include 'bi/x-circle-fill.html' %}</span>
+                                    {% endif %}
+                                </td>
+ 
+                            </tr>
+                        {% empty %}
+ 
+                            <tr>
+ 
+                                <td colspan="4">No Profiles found</td>
+ 
+                            </tr>
+ 
+                        {% endfor %}
+	
+                    </tbody>
+                </table>
+
+                {% if is_paginated %}
+	
+                    <div class="col-12">{% include '_pagination.html' with page_obj=page_obj %}</div>
+                {% endif %}
+
+            </div>
+        </div>
+    {% endblock content %}
+
+    {% block footer_script %}
+        <script src="{% static 'scipost/table-row.js' %}"></script>
+    {% endblock footer_script %}
diff --git a/scipost_django/scipost/forms.py b/scipost_django/scipost/forms.py
index 3691701b13b044275b2053be23996be412ca1ed9..6daea1fc8a0711643983eb88c850e148bc874e1b 100644
--- a/scipost_django/scipost/forms.py
+++ b/scipost_django/scipost/forms.py
@@ -14,6 +14,7 @@ from django.contrib.auth.password_validation import validate_password
 from django.contrib.auth.validators import UnicodeUsernameValidator
 from django.core.exceptions import ValidationError
 from django.http import Http404
+from django.urls import reverse
 from django.utils import timezone
 from django.utils.dates import MONTHS
 
@@ -643,12 +644,55 @@ class UnavailabilityPeriodForm(forms.ModelForm):
 
 class ContributorMergeForm(forms.Form):
     to_merge = ModelChoiceFieldwithid(
-        queryset=Contributor.objects.all(), empty_label=None
+        queryset=Contributor.objects.all(),
+        empty_label=None,
+        label="Merge this contributor",
     )
     to_merge_into = ModelChoiceFieldwithid(
-        queryset=Contributor.objects.all(), empty_label=None
+        queryset=Contributor.objects.all(),
+        empty_label=None,
+        label="Into this contributor",
     )
 
+    def __init__(self, *args, **kwargs):
+        queryset = kwargs.pop("queryset", None)
+        super().__init__(*args, **kwargs)
+        if queryset:
+            self.fields["to_merge"].queryset = queryset
+            self.fields["to_merge_into"].queryset = queryset
+
+        self.helper = FormHelper()
+        self.helper.attrs = {
+            "hx-target": "#merge-form-info",
+            "hx-get": reverse(
+                "scipost:_hx_contributor_comparison",
+            ),
+            "hx-trigger": "intersect once, change from:select",
+        }
+        self.layout = Layout(
+            Div(
+                Div(
+                    Field("to_merge"),
+                    css_id="to_merge",
+                    css_class="col-12 col-md",
+                ),
+                Div(
+                    Field("to_merge_into"),
+                    css_id="to_merge_into",
+                    css_class="col-12 col-md",
+                ),
+                css_class="row mb-0",
+            ),
+            Div(
+                Div(
+                    css_class="col-12",
+                    css_id="merge-form-info",
+                ),
+                css_class="row mb-0",
+            ),
+        )
+        self.helper.layout = self.layout
+
     def clean(self):
         data = super().clean()
         if self.cleaned_data["to_merge"] == self.cleaned_data["to_merge_into"]:
diff --git a/scipost_django/scipost/permissions.py b/scipost_django/scipost/permissions.py
new file mode 100644
index 0000000000000000000000000000000000000000..09e6e7e73510cd0a0465459417f0beaf54f24067
--- /dev/null
+++ b/scipost_django/scipost/permissions.py
@@ -0,0 +1,54 @@
+from functools import wraps
+
+from django.contrib import messages
+from django.http import HttpResponse
+
+####################
+# HTMX inline alerts
+####################
+
+
+class HTMXResponse(HttpResponse):
+    tag = "primary"
+    message = ""
+    css_class = ""
+
+    def __init__(self, *args, **kwargs):
+        tag = kwargs.pop("tag", self.tag)
+        message = args[0] if args else kwargs.pop("message", self.message)
+        css_class = kwargs.pop("css_class", self.css_class)
+
+        alert_html = f"""<div class="text-{tag} border border-{tag} p-3 {css_class}">
+                {message}
+            </div>"""
+
+        super().__init__(alert_html, *args, **kwargs)
+
+
+class HTMXPermissionsDenied(HTMXResponse):
+    tag = "danger"
+    message = "You do not have the required permissions."
+
+
+def permission_required_htmx(
+    perm,
+    message="You do not have the required permissions.",
+    **message_kwargs,
+):
+    def decorator(view_func):
+        @wraps(view_func)
+        def _wrapped_view(request, *args, **kwargs):
+            if isinstance(perm, str):
+                perms = (perm,)
+            else:
+                perms = perm
+
+            if request.user.has_perms(perms):
+                return view_func(request, *args, **kwargs)
+            else:
+                messages.error(request, message)
+                return HTMXPermissionsDenied(message, **message_kwargs)
+
+        return _wrapped_view
+
+    return decorator
diff --git a/scipost_django/scipost/templates/scipost/_hx_contributor_comparison.html b/scipost_django/scipost/templates/scipost/_hx_contributor_comparison.html
new file mode 100644
index 0000000000000000000000000000000000000000..16f0951616b9496f8d3626d3e75315a23a4b6039
--- /dev/null
+++ b/scipost_django/scipost/templates/scipost/_hx_contributor_comparison.html
@@ -0,0 +1,33 @@
+{% load crispy_forms_tags %}
+{% load scipost_extras %}
+
+<div class="row">
+  <div class="col-12 col-md-6">
+    <h3 class="highlight">Merge Contributor {{ contributor_to_merge.id }}</h3>
+    {% include "scipost/_public_info_as_table.html" with contributor=contributor_to_merge %}
+  </div>
+  <div class="col-12 col-md-6">
+    <h3 class="highlight">into Contributor {{ contributor_to_merge_into.id }}</h3>
+    {% include "scipost/_public_info_as_table.html" with contributor=contributor_to_merge_into %}
+  </div>
+</div>
+
+<div class="row">
+  <div class="col-12">
+    {% if contributor_to_merge.user.is_active and not contributor_to_merge_into.user.is_active %}
+      <h3 class="text-danger">
+        Warning: the contributor to merge is active, while the one to merge into is not. Consider swapping the order with "Swap & Merge".
+      </h3>
+      <div id="contributor-swap-merge-btn"
+           class="btn btn-warning me-2"
+           hx-post="{% url "scipost:_hx_contributor_merge" to_merge=contributor_to_merge.id to_merge_into=contributor_to_merge_into.id %}">
+        Swap & Merge
+      </div>
+    {% endif %}
+    <div id="contributor-merge-btn"
+         class="btn btn-primary"
+         hx-post="{% url "scipost:_hx_contributor_merge" to_merge=contributor_to_merge_into.id to_merge_into=contributor_to_merge.id %}">
+      Merge
+    </div>
+  </div>
+</div>
diff --git a/scipost_django/scipost/templates/scipost/_hx_contributor_duplicate_merger.html b/scipost_django/scipost/templates/scipost/_hx_contributor_duplicate_merger.html
new file mode 100644
index 0000000000000000000000000000000000000000..ab894f96603176d85ca63585585600e80be00bb1
--- /dev/null
+++ b/scipost_django/scipost/templates/scipost/_hx_contributor_duplicate_merger.html
@@ -0,0 +1,24 @@
+{% load crispy_forms_tags %}
+
+<div class="row mt-4">
+  <div class="d-none d-lg-block col-3">
+    <h3>All found duplicates</h3>
+    <div style='max-height: 50vh' class="overflow-scroll">
+      {% for contributor in duplicate_contributors %}
+        <li>
+          <div class="text-nowrap text-truncate" title="{{ contributor }}">
+            {{ contributor }} (<em>id={{ contributor.id }}</em>)
+          </div>
+        </li>
+      {% empty %}
+        <em>No duplicates found</em>
+      {% endfor %}
+    </div>
+  </div>
+
+  <div class="col">
+    {% if form %}
+      {% crispy form %}
+    {% endif %}
+  </div>
+</div>
diff --git a/scipost_django/scipost/templates/scipost/contributor_duplicate_list.html b/scipost_django/scipost/templates/scipost/contributor_duplicate_list.html
deleted file mode 100644
index 635fe084e60e3cd144328132626c037422a0e35c..0000000000000000000000000000000000000000
--- a/scipost_django/scipost/templates/scipost/contributor_duplicate_list.html
+++ /dev/null
@@ -1,44 +0,0 @@
-{% extends 'profiles/base.html' %}
-
-{% load bootstrap %}
-
-{% block breadcrumb_items %}
-  {{ block.super }}
-  <span class="breadcrumb-item">Contributor duplicates</span>
-{% endblock %}
-
-{% load scipost_extras %}
-
-{% block pagetitle %}: Contributor duplicates{% endblock pagetitle %}
-
-{% block content %}
-  <div class="row">
-    <div class="col-12">
-      <h1 class="highlight">Potentially duplicate Contributors</h1>
-      {% if merge_form %}
-        <form action="{% url 'scipost:contributor_merge' %}" method="get">
-          {{ merge_form|bootstrap }}
-          <input class="btn btn-outline-secondary" type="submit" value="Check">
-        </form>
-      {% endif %}
-
-      <br>
-      <h3>All found duplicates</h3>
-      <ul>
-	{% for contrib_dup in object_list %}
-	  <li>{{ contrib_dup }} (<em>id={{ contrib_dup.id }}</em>)</li>
-	{% empty %}
-          <li<em>No duplicates found</em></li>
-	{% endfor %}
-      </ul>
-
-      {% if is_paginated %}
-	<div class="col-12">
-	  {% include '_pagination.html' with page_obj=page_obj %}
-	</div>
-      {% endif %}
-
-    </div>
-  </div>
-
-{% endblock content %}
diff --git a/scipost_django/scipost/templates/scipost/contributor_duplicates.html b/scipost_django/scipost/templates/scipost/contributor_duplicates.html
new file mode 100644
index 0000000000000000000000000000000000000000..c70db0a23bcfb6ec357413c063abda4a79cf7228
--- /dev/null
+++ b/scipost_django/scipost/templates/scipost/contributor_duplicates.html
@@ -0,0 +1,25 @@
+{% extends 'profiles/base.html' %}
+
+{% block breadcrumb_items %}
+  {{ block.super }}
+  <span class="breadcrumb-item">Contributor duplicates</span>
+{% endblock %}
+
+{% load scipost_extras %}
+
+{% block pagetitle %}
+  : Contributor duplicates
+{% endblock pagetitle %}
+
+{% block content %}
+  <div class="row">
+    <div class="col-12">
+      <h1 class="highlight">Potentially duplicate Contributors</h1>
+
+      <div hx-trigger="intersect once, htmx:trigger from:body target:.btn delay:1000"
+           hx-get="{% url 'scipost:_hx_contributor_duplicate_merger' %}?kind={{ group_by }}"></div>
+
+    </div>
+  </div>
+
+{% endblock content %}
diff --git a/scipost_django/scipost/templates/scipost/contributor_merge.html b/scipost_django/scipost/templates/scipost/contributor_merge.html
deleted file mode 100644
index b74612df222822bd54efaad9b57e96e7e055396c..0000000000000000000000000000000000000000
--- a/scipost_django/scipost/templates/scipost/contributor_merge.html
+++ /dev/null
@@ -1,50 +0,0 @@
-{% extends 'scipost/base.html' %}
-
-{% load bootstrap %}
-
-{% block breadcrumb_items %}
-  {{ block.super }}
-  <span class="breadcrumb-item"><a href="{% url 'scipost:contributor_duplicates' %}">Duplicates</a></span>
-  <span class="breadcrumb-item">Merge Contributors {{ contributor_to_merge.id }} and {{ contributor_to_merge_into.id }}</span>
-{% endblock %}
-
-{% load scipost_extras %}
-
-{% block pagetitle %}: Contributor duplicates: merge{% endblock pagetitle %}
-
-{% block content %}
-  <div class="row">
-    <div class="col-12">
-      <h1 class="highlight">Merge Contributors {{ contributor_to_merge.id }} and {{ contributor_to_merge_into.id }}</h1>
-      {% if contributor_to_merge.user.is_active and not contributor_to_merge_into.user.is_active %}
-	<h3 class="text-danger">Warning: the contributor to merge is active, while the one to merge into is not</h3>
-	<p>Consider <a href="{% url 'scipost:contributor_merge' %}?to_merge={{ contributor_to_merge_into.id }}&to_merge_into={{ contributor_to_merge.id }}" method="get">merging the other way around</a></p>
-      {% endif %}
-    </div>
-  </div>
-  <div class="row">
-    <div class="col-12">
-      <h3 class="highlight">Contributor {{ contributor_to_merge.id }}</h3>
-      {% include "scipost/_public_info_as_table.html" with contributor=contributor_to_merge %}
-    </div>
-  </div>
-  <div class="row">
-    <div class="col-12">
-      <h3 class="highlight">Contributor {{ contributor_to_merge_into.id }}</h3>
-      {% include "scipost/_public_info_as_table.html" with contributor=contributor_to_merge_into %}
-    </div>
-  </div>
-
-  <div class="row">
-    <div class="col-12">
-      <h3 class="highlight">Merge:</h3>
-      <form method="post">
-	{% csrf_token %}
-	{{ merge_form|bootstrap }}
-	<input class="btn btn-primary" type="submit" value="Confirm merge">
-	&nbsp;<a class="text-warning" href="{% url 'scipost:contributor_merge' %}?to_merge={{ contributor_to_merge_into.id }}&to_merge_into={{ contributor_to_merge.id }}" method="get">Merge the other way around</a></p>
-      </form>
-    </div>
-  </div>
-
-{% endblock content %}
diff --git a/scipost_django/scipost/urls.py b/scipost_django/scipost/urls.py
index f1f4cd38541933820d6630bcd325115ade047a06..371ccd1decb82bc32df89f510f9344bb5ae4c749 100644
--- a/scipost_django/scipost/urls.py
+++ b/scipost_django/scipost/urls.py
@@ -463,11 +463,25 @@ urlpatterns = [
     # Potential duplicates
     #######################
     path(
-        "contributor_duplicates/",
-        views.ContributorDuplicateListView.as_view(),
+        "contributor_duplicates/<str:group_by>",
+        views.contributor_duplicates,
         name="contributor_duplicates",
     ),
-    path("contributor_merge/", views.contributor_merge, name="contributor_merge"),
+    path(
+        "_hx_contributor_duplicate_merger",
+        views.ContributorDuplicateListView.as_view(),
+        name="_hx_contributor_duplicate_merger",
+    ),
+    path(
+        "_hx_contributor_comparison",
+        views._hx_contributor_comparison,
+        name="_hx_contributor_comparison",
+    ),
+    path(
+        "_hx_contributor_merge/<int:to_merge>/<int:to_merge_into>",
+        views._hx_contributor_merge,
+        name="_hx_contributor_merge",
+    ),
     #
     ###################
     # Email facilities
diff --git a/scipost_django/scipost/views.py b/scipost_django/scipost/views.py
index 34b196241ec688b631c42198e782e480f7ea5dae..56f8684738251cbfa46e1eabd50451a04be590c0 100644
--- a/scipost_django/scipost/views.py
+++ b/scipost_django/scipost/views.py
@@ -42,6 +42,7 @@ from django.views.static import serve
 
 from dal import autocomplete
 from guardian.decorators import permission_required
+from scipost.permissions import permission_required_htmx, HTMXResponse
 
 from .constants import SciPost_from_addresses_dict, NORMAL_CONTRIBUTOR
 from .decorators import has_contributor, is_contributor_user
@@ -182,33 +183,6 @@ def _hx_messages(request):
     return render(request, "scipost/_hx_messages.html")
 
 
-####################
-# HTMX inline alerts
-####################
-
-
-class HTMXResponse(HttpResponse):
-    tag = "primary"
-    message = ""
-    css_class = ""
-
-    def __init__(self, *args, **kwargs):
-        tag = kwargs.pop("tag", self.tag)
-        message = args[0] if args else kwargs.pop("message", self.message)
-        css_class = kwargs.pop("css_class", self.css_class)
-
-        alert_html = f"""<div class="text-{tag} border border-{tag} p-3 {css_class}">
-                {message}
-            </div>"""
-
-        super().__init__(alert_html, *args, **kwargs)
-
-
-class HTMXPermissionsDenied(HTMXResponse):
-    tag = "danger"
-    message = "You do not have the required permissions."
-
-
 #############
 # Main view
 #############
@@ -1562,7 +1536,13 @@ def contributor_info(request, contributor_id):
     return render(request, "scipost/contributor_info.html", context)
 
 
-class ContributorDuplicateListView(PermissionsMixin, PaginationMixin, ListView):
+def contributor_duplicates(request, group_by: str):
+    return render(
+        request, "scipost/contributor_duplicates.html", {"group_by": group_by}
+    )
+
+
+class ContributorDuplicateListView(PermissionsMixin, ListView):
     """
     List Contributors with potential (not yet handled) duplicates.
     Two sources of duplicates are separately considered:
@@ -1574,7 +1554,7 @@ class ContributorDuplicateListView(PermissionsMixin, PaginationMixin, ListView):
 
     permission_required = "scipost.can_vet_registration_requests"
     model = Contributor
-    template_name = "scipost/contributor_duplicate_list.html"
+    template_name = "scipost/_hx_contributor_duplicate_merger.html"
 
     def get_queryset(self):
         queryset = Contributor.objects.all()
@@ -1588,19 +1568,25 @@ class ContributorDuplicateListView(PermissionsMixin, PaginationMixin, ListView):
 
     def get_context_data(self, *args, **kwargs):
         context = super().get_context_data(*args, **kwargs)
+        context["duplicate_contributors"] = context.pop("object_list")
 
-        if len(context["object_list"]) > 1:
+        if len(context["duplicate_contributors"]) > 1:
             initial = {
-                "to_merge": context["object_list"][0].id,
-                "to_merge_into": context["object_list"][1].id,
+                "to_merge": context["duplicate_contributors"][0].id,
+                "to_merge_into": context["duplicate_contributors"][1].id,
             }
-            context["merge_form"] = ContributorMergeForm(initial=initial)
+            context["form"] = ContributorMergeForm(
+                initial=initial, queryset=self.get_queryset()
+            )
         return context
 
 
 @transaction.atomic
-@permission_required("scipost.can_vet_registration_requests")
-def contributor_merge(request):
+@permission_required_htmx(
+    "scipost.can_vet_registration_requests",
+    "You do not have permission to vet registration requests.",
+)
+def _hx_contributor_comparison(request):
     """
     Handles the merging of data from one Contributor instance to another,
     to solve one person - multiple registrations issues.
@@ -1611,45 +1597,60 @@ def contributor_merge(request):
     If both Contributor instances were active, then the account owner
     is emailed with information about the merge.
     """
-    merge_form = ContributorMergeForm(request.POST or None, initial=request.GET)
-    context = {"merge_form": merge_form}
+
+    if request.method == "GET":
+        try:
+            context = {
+                "contributor_to_merge": get_object_or_404(
+                    Contributor, pk=int(request.GET["to_merge"])
+                ),
+                "contributor_to_merge_into": get_object_or_404(
+                    Contributor, pk=int(request.GET["to_merge_into"])
+                ),
+            }
+        except ValueError:
+            raise Http404
+
+    return render(request, "scipost/_hx_contributor_comparison.html", context)
+
+
+@transaction.atomic
+@permission_required_htmx(
+    "scipost.can_vet_registration_requests",
+    "You do not have permission to vet registration requests.",
+)
+def _hx_contributor_merge(request, to_merge: int, to_merge_into: int):
+    """
+    Confirms the merging of data from one Contributor instance to another,
+    to solve one person - multiple registrations issues.
+    """
+
+    merge_form = ContributorMergeForm(
+        request.POST or None,
+        initial={
+            "to_merge": to_merge,
+            "to_merge_into": to_merge_into,
+        },
+        queryset=Contributor.objects.filter(id__in=[to_merge, to_merge_into]),
+    )
 
     if request.method == "POST":
         if merge_form.is_valid():
             contributor = merge_form.save()
-            messages.success(request, "Contributors merged")
-            return redirect(reverse("scipost:contributor_duplicates"))
-        else:
-            try:
-                context.update(
-                    {
-                        "contributor_to_merge": get_object_or_404(
-                            Contributor, pk=merge_form.cleaned_data["to_merge"].id
-                        ),
-                        "contributor_to_merge_into": get_object_or_404(
-                            Contributor, pk=merge_form.cleaned_data["to_merge_into"].id
-                        ),
-                    }
-                )
-            except ValueError:
-                raise Http404
-
-    elif request.method == "GET":
-        try:
-            context.update(
-                {
-                    "contributor_to_merge": get_object_or_404(
-                        Contributor, pk=int(request.GET["to_merge"])
-                    ),
-                    "contributor_to_merge_into": get_object_or_404(
-                        Contributor, pk=int(request.GET["to_merge_into"])
-                    ),
-                }
+            messages.success(request, "Contributors merged successfully.")
+            return HTMXResponse(
+                f"Contributors {to_merge} and {to_merge_into} merged into {contributor.id}.",
+            )
+        elif to_merge == to_merge_into:
+            return HTMXResponse(
+                "Cannot merge a Contributor into itself.",
+                tag="danger",
             )
-        except ValueError:
-            raise Http404
 
-    return render(request, "scipost/contributor_merge.html", context)
+    return HTMXResponse(
+        "Failed to merge contributors.",
+        tag="danger",
+    )
 
 
 ####################