diff --git a/src/api/views/maps.py b/src/api/views/maps.py
index 77b9209605e3e6e19f90e07056ac0e5ce795a491..b84b3288c0acc0f7bda16e48bebfb5f1d5fd6b9e 100644
--- a/src/api/views/maps.py
+++ b/src/api/views/maps.py
@@ -17,11 +17,13 @@ from api.views.mixins import ConferenceSlugMixin
 _cts = {}  # cache of CoordTransforms (if needed)
 
 
+q_publishable_location_states = Q(location_state__in=[Assembly.LocationState.PREVIEW, Assembly.LocationState.FINAL])
+
+
 def get_exportable_assemblies(conference: Conference) -> QuerySet[Assembly]:
     """Fetches all assemblies in the given conference with publish-able location."""
     exportable_states = [*Assembly.PUBLIC_STATES, Assembly.State.HIDDEN]
-    q_placed_assemblies = Q(location_state__in=[Assembly.LocationState.PREVIEW, Assembly.LocationState.FINAL])
-    return Assembly.objects.filter(conference=conference, state__in=exportable_states).filter(q_placed_assemblies)
+    return Assembly.objects.filter(conference=conference, state__in=exportable_states).filter(q_publishable_location_states)
 
 
 def get_field_geojson(value, srid: int, ct_cache: dict | None = None):
@@ -157,15 +159,17 @@ class C3NavExportView(ConferenceSlugMixin, APIView):
                     'name': assembly.name,
                     'is_official': assembly.is_official,
                     'description': {'de': assembly.description_de, 'en': assembly.description_en},
-                    'public_url': hub_absolute('plainui:assembly', assembly_slug=assembly.slug),
+                    'public_url': hub_absolute('plainui:assembly', assembly_slug=assembly.slug, i18n=False),
                     'parent_id': assembly.parent_id,
                     'children': get_exportable_assemblies(conference=self.conference).filter(parent=assembly).values_list('slug', flat=True)
                     if assembly.is_cluster
                     else None,
+                    'rooms': assembly.rooms.filter(q_publishable_location_states).exclude(blocked=True).values_list('slug', flat=True),
                     'floor': assembly.get_location_floor_index(),
                     'is_preview': assembly.location_state != Assembly.LocationState.FINAL,
                     'location': loc_data.get('point'),  # assembly.get_location_point_xy(),
                     'polygons': loc_data.get('boundaries'),  # assembly.get_location_boundaries_xy(),
+                    'tags': assembly.tags.filter(tag__is_public=True).values_list('tag__slug', flat=True),
                 }
             )
 
@@ -177,14 +181,34 @@ class C3NavExportView(ConferenceSlugMixin, APIView):
                         'slug': project.slug,
                         'name': project.name,
                         'description': {'de': project.description_de, 'en': project.description_en},
-                        'public_url': hub_absolute('plainui:project', slug=project.slug),
+                        'public_url': hub_absolute('plainui:project', slug=project.slug, i18n=False),
                         'assembly_id': str(assembly.pk),
                         'floor': assembly.get_location_floor_index(),  # TODO: allow project-specific location floor
+                        'is_preview': assembly.location_state != Assembly.LocationState.FINAL,
                         'location': loc_data.get('point'),  # TODO: allow project-specific location point
                         'location_text': project.location,
+                        'tags': project.tags.filter(tag__is_public=True).values_list('tag__slug', flat=True),
                     }
                 )
 
+        for room in self.conference.rooms.filter(q_publishable_location_states).exclude(blocked=True):
+            data.append(
+                {
+                    'type': 'room',
+                    'id': str(room.pk),
+                    'name': room.name,
+                    'is_official': room.is_official,
+                    'in_public_fahrplan': room.is_public_fahrplan,
+                    'description': {'de': room.description_de, 'en': room.description_en},
+                    'public_url': hub_absolute('plainui:room', slug=room.slug, i18n=False),
+                    'is_preview': room.location_state != Assembly.LocationState.FINAL,
+                    'floor': room.get_location_floor_index(),
+                    'location': room.get_location_point_xy(),
+                    'polygons': room.get_location_boundaries_xy(),
+                    'tags': room.tags.filter(tag__is_public=True).values_list('tag__slug', flat=True),
+                }
+            )
+
         for poi in self.conference.pois.filter(visible=True):  # type: MapPOI
             data.append(
                 {
@@ -193,6 +217,7 @@ class C3NavExportView(ConferenceSlugMixin, APIView):
                     'name': {'de': poi.name_de, 'en': poi.name_en},
                     'is_official': poi.is_official,
                     'description': {'de': poi.description_de, 'en': poi.description_en},
+                    'is_preview': False,
                     'floor': poi.get_location_floor_index(),
                     'location': poi.get_location_point_xy(),
                 }
diff --git a/src/backoffice/locale/de/LC_MESSAGES/django.po b/src/backoffice/locale/de/LC_MESSAGES/django.po
index 89ce478d592bcb0675a2fe010a746fe16bb68a47..fe2cb0db938a0fca183f517592fe9de4423e6bf9 100644
--- a/src/backoffice/locale/de/LC_MESSAGES/django.po
+++ b/src/backoffice/locale/de/LC_MESSAGES/django.po
@@ -665,32 +665,26 @@ msgstr "Assembly bearbeiten"
 msgid "AssemblyTeam__message"
 msgstr "Nachricht senden"
 
-msgid "AssemblyTeam__position__status"
-msgstr "Status"
-
-msgid "AssemblyTeam__position__point"
-msgstr "POI"
-
-msgid "AssemblyTeam__position__boundary"
-msgstr "Umrandung"
-
-msgid "AssemblyTeam__position-final"
-msgstr "final"
+msgid "AssemblyTeam__lastnote"
+msgstr "letzte Notiz"
 
-msgid "AssemblyTeam__position-preview"
-msgstr "Vorschau"
+# use translation from core
+msgid "Rooms"
+msgstr "Raum"
 
-msgid "AssemblyTeam__position-draft"
-msgstr "Entwurf"
+# use translation from core
+msgid "Room__blocked__help"
+msgstr ""
 
-msgid "AssemblyTeam__position-internal"
-msgstr "nur Assembly"
+# use translation from core
+msgid "Room__blocked"
+msgstr ""
 
-msgid "AssemblyTeam__position-none"
-msgstr "nicht verf."
+msgid "Room__location_point"
+msgstr "Position (Karte)"
 
-msgid "AssemblyTeam__lastnote"
-msgstr "letzte Notiz"
+msgid "Room__edit__position"
+msgstr "Position des Raums bearbeiten"
 
 msgid "Assembly__edit__hierarchy"
 msgstr "Hierarchie/Art der Assembly ändern"
@@ -723,6 +717,13 @@ msgstr "Verwaltende der Assemblies informieren"
 msgid "AssemblyTeam__edit_position-save"
 msgstr "Position speichern im Status:"
 
+# use translation from core
+msgid "Room__location_floor"
+msgstr ""
+
+msgid "Room__location_boundaries"
+msgstr "Bereiche"
+
 msgid "AssemblyTeam__edit_position__tooltip-none"
 msgstr "Als 'keine Position' behandeln, auch wenn ggf. bereits eine gespeichert ist - z.B. ein alter Standort."
 
@@ -779,6 +780,30 @@ msgstr "Nachrichten Empfänger"
 msgid "AssemblyTeam__message__send_button"
 msgstr "Nachricht versenden"
 
+msgid "AssemblyTeam__position__status"
+msgstr "Status"
+
+msgid "AssemblyTeam__position__point"
+msgstr "POI"
+
+msgid "AssemblyTeam__position__boundary"
+msgstr "Umrandung"
+
+msgid "AssemblyTeam__position-final"
+msgstr "final"
+
+msgid "AssemblyTeam__position-preview"
+msgstr "Vorschau"
+
+msgid "AssemblyTeam__position-draft"
+msgstr "Entwurf"
+
+msgid "AssemblyTeam__position-internal"
+msgstr "nur Assembly"
+
+msgid "AssemblyTeam__position-none"
+msgstr "nicht verf."
+
 msgid "nav_active_tab_sr_marker"
 msgstr "(ausgewählt)"
 
diff --git a/src/backoffice/locale/en/LC_MESSAGES/django.po b/src/backoffice/locale/en/LC_MESSAGES/django.po
index 82947e614461b264e496cb4e90c3f05195999048..3c10871739105cefd64e89e40e6bcb5a66ae58c0 100644
--- a/src/backoffice/locale/en/LC_MESSAGES/django.po
+++ b/src/backoffice/locale/en/LC_MESSAGES/django.po
@@ -665,32 +665,26 @@ msgstr "edit assembly"
 msgid "AssemblyTeam__message"
 msgstr "send a message"
 
-msgid "AssemblyTeam__position__status"
-msgstr "status"
-
-msgid "AssemblyTeam__position__point"
-msgstr "POI"
-
-msgid "AssemblyTeam__position__boundary"
-msgstr "boundary"
-
-msgid "AssemblyTeam__position-final"
-msgstr "final"
+msgid "AssemblyTeam__lastnote"
+msgstr "latest note"
 
-msgid "AssemblyTeam__position-preview"
-msgstr "preview"
+# use translation from core
+msgid "Rooms"
+msgstr ""
 
-msgid "AssemblyTeam__position-draft"
-msgstr "draft"
+# use translation from core
+msgid "Room__blocked__help"
+msgstr ""
 
-msgid "AssemblyTeam__position-internal"
-msgstr "assembly"
+# use translation from core
+msgid "Room__blocked"
+msgstr ""
 
-msgid "AssemblyTeam__position-none"
-msgstr "none"
+msgid "Room__location_point"
+msgstr "POI"
 
-msgid "AssemblyTeam__lastnote"
-msgstr "latest note"
+msgid "Room__edit__position"
+msgstr "placement of the room"
 
 msgid "Assembly__edit__hierarchy"
 msgstr "change assembly's categorization"
@@ -723,6 +717,13 @@ msgstr "inform the assembly's management"
 msgid "AssemblyTeam__edit_position-save"
 msgstr "save the position with this state:"
 
+# use translation from core
+msgid "Room__location_floor"
+msgstr ""
+
+msgid "Room__location_boundaries"
+msgstr "boundaries"
+
 msgid "AssemblyTeam__edit_position__tooltip-none"
 msgstr "handle this assembly as 'no position', even if a location may be set (e.g. old one)"
 
@@ -779,6 +780,30 @@ msgstr "message recipients"
 msgid "AssemblyTeam__message__send_button"
 msgstr "Send the message"
 
+msgid "AssemblyTeam__position__status"
+msgstr "status"
+
+msgid "AssemblyTeam__position__point"
+msgstr "POI"
+
+msgid "AssemblyTeam__position__boundary"
+msgstr "boundary"
+
+msgid "AssemblyTeam__position-final"
+msgstr "final"
+
+msgid "AssemblyTeam__position-preview"
+msgstr "preview"
+
+msgid "AssemblyTeam__position-draft"
+msgstr "draft"
+
+msgid "AssemblyTeam__position-internal"
+msgstr "assembly"
+
+msgid "AssemblyTeam__position-none"
+msgstr "none"
+
 msgid "nav_active_tab_sr_marker"
 msgstr "(current)"
 
diff --git a/src/backoffice/templates/backoffice/assemblyteam_assembly_detail.html b/src/backoffice/templates/backoffice/assemblyteam_assembly_detail.html
index 64df76b5d2aec3a7236e8381157ee59a2c4e0878..2fc1e5149665d2246259c5cd0d37f40900f360b5 100644
--- a/src/backoffice/templates/backoffice/assemblyteam_assembly_detail.html
+++ b/src/backoffice/templates/backoffice/assemblyteam_assembly_detail.html
@@ -210,75 +210,8 @@
       </div>
 
       {% if conference.support_assembly_physical %}
-        <div class="card mb-3">
-          <div class="card-header">Position</div>
-          <div class="card-body d-flex flex-row">
-            <div>
-              <label for="pos_public">
-                <i class="bi bi-eye{% if not object.is_placed %}-slash{% endif %}"></i> {% trans "AssemblyTeam__position__status" %}:
-              </label>
-              <br>
-              <label for="pos_point">
-                <i class="bi bi-pin-map"></i> {% trans "AssemblyTeam__position__point" %}:
-              </label>
-              <br>
-              <label for="pos_boundary">
-                <i class="bi bi-map"></i> {% trans "AssemblyTeam__position__boundary" %}
-              </label>
-            </div>
-            <div class="flex-grow-1 ps-3">
-              {% if object.location_state == object.LocationState.FINAL %}
-                <span id="pos_public"
-                      class="text-success"
-                      title="{{ object.get_location_state_display }}"><i class="bi bi-check-square"></i> {% trans "AssemblyTeam__position-final" %}</span>
-              {% elif object.location_state == object.LocationState.PREVIEW %}
-                <span id="pos_public"
-                      class="text-success"
-                      title="{{ object.get_location_state_display }}"><i class="bi bi-check-square"></i> {% trans "AssemblyTeam__position-preview" %}</span>
-              {% elif object.location_state == object.LocationState.DRAFT %}
-                <span id="pos_public"
-                      class="text-warning"
-                      title="{{ object.get_location_state_display }}"><i class="bi bi-x-square"></i> {% trans "AssemblyTeam__position-draft" %}</span>
-              {% elif object.location_state == object.LocationState.ROUGH %}
-                <span id="pos_public"
-                      class="text-warning"
-                      title="{{ object.get_location_state_display }}"><i class="bi bi-x-square"></i> {% trans "AssemblyTeam__position-internal" %}</span>
-              {% else %}
-                <span id="pos_public"
-                      class="text-danger"
-                      title="{{ object.get_location_state_display }}"><i class="bi bi-x-square"></i> {% trans "AssemblyTeam__position-none" %}</span>
-              {% endif %}
-              <br>
-              {% if object.location_data.point %}
-                <span id="pos_point"
-                      class="text-success"
-                      title="{{ object.location_data.point }}">
-                  <i class="bi bi-check-square"></i> {% trans "yes" %}
-                </span>
-              {% else %}
-                <span id="pos_poi" class="text-danger"><i class="bi bi-x-square"></i> {% trans "no" %}</span>
-              {% endif %}
-              <br>
-              {% if object.location_data.boundaries %}
-                <span id="pos_boundary"
-                      class="text-success"
-                      title="{{ object.location_data.boundaries }}">
-                  <i class="bi bi-check-square"></i> {% trans "yes" %}
-                </span>
-              {% else %}
-                <span id="pos_boundary" class="text-danger"><i class="bi bi-x-square"></i> {% trans "no" %}</span>
-              {% endif %}
-            </div>
-            <div>
-              <a role="button"
-                 class="btn btn-sm btn-{% if assembly.is_public %}primary{% else %}secondary{% endif %}"
-                 href="{% url 'backoffice:assemblyteam-editposition' pk=assembly.id %}">
-                <i class="bi bi-pencil"></i>
-                {% trans "edit" %}
-              </a>
-            </div>
-          </div>
-        </div>
+        {% url 'backoffice:assemblyteam-editposition-assembly' pk=assembly.id as position_edit_url %}
+        {% include "backoffice/assemblyteam_position_info_card.html" %}
       {% endif %}
 
       {% if latest_note %}
@@ -295,6 +228,32 @@
           </div>
         </div>
       {% endif %}
+
+      <div class="card">
+        <div class="card-header">{% trans "Rooms" %}</div>
+        <div class="card-body">
+          {% for room in assembly.rooms.all %}
+            <span class="fs-0 text-muted">
+              {{ room.get_room_type_display }}
+              <abbr title="{{ room.name }}">{{ room.slug }}</abbr>
+              {% if room.blocked %}
+                <abbr class="text-danger" title="{% trans "Room__blocked__help" %}">{% trans "Room__blocked" %}</abbr>
+              {% endif %}
+            </span>
+            <br />
+            <span class="text-{% if room.location_state == room.assembly.LocationState.NONE %}danger{% elif room.location_state == room.assembly.LocationState.FINAL %}success{% else %}warning{% endif %}">
+              <abbr title="{% trans "Room__location_point" %}"><i class="bi bi-pin-map-fill"></i></abbr>
+              {{ room.get_location_state_display }}
+            </span>
+            <a class="btn btn-sm btn-outline-primary"
+               href="{% url "backoffice:assemblyteam-editposition-room" pk=room.pk %}"
+               title="{% trans "Room__edit__position" %}"><i class="bi bi-pencil"></i></a>
+            {% if not forloop.last %}<hr>{% endif %}
+          {% empty %}
+            <span class="text-muted">-/-</span>
+          {% endfor %}
+        </div>
+      </div>
     </div>
   </div>
 
diff --git a/src/backoffice/templates/backoffice/assemblyteam_editposition.html b/src/backoffice/templates/backoffice/assemblyteam_editposition_assembly.html
similarity index 55%
rename from src/backoffice/templates/backoffice/assemblyteam_editposition.html
rename to src/backoffice/templates/backoffice/assemblyteam_editposition_assembly.html
index c12dde635e12dc0808157e425b0f82410d96a6da..6a60fd7c8aac2a8cac384321b6b44a4c6a57451a 100644
--- a/src/backoffice/templates/backoffice/assemblyteam_editposition.html
+++ b/src/backoffice/templates/backoffice/assemblyteam_editposition_assembly.html
@@ -17,7 +17,7 @@
       <div class="card mb-3">
         <div class="card-header">{% trans "Assembly__edit__position" %}</div>
         <div class="card-body">
-          <form action="{% url 'backoffice:assemblyteam-editposition' pk=assembly.id %}"
+          <form action="{% url 'backoffice:assemblyteam-editposition-assembly' pk=assembly.id %}"
                 method="post">
             {% csrf_token %}
             <input type="hidden" name="value" value="{{ new_value }}">
@@ -81,41 +81,7 @@
 
             <label class="col-sm-2 col-form-label text-muted" for="buttons">{% trans "AssemblyTeam__edit_position-save" %}</label>
             <div class="btn-group" id="buttons">
-              <button type="submit"
-                      class="btn btn-sm {% if assembly.location_state == 'none' %}btn-primary{% else %}btn-outline-danger{% endif %}"
-                      name="location_state"
-                      value="none"
-                      title="{% trans "AssemblyTeam__edit_position__tooltip-none" %}">
-                {% trans "Assembly__location_state-none" %}
-              </button>
-              <button type="submit"
-                      class="btn btn-sm {% if assembly.location_state == 'draft' %}btn-primary{% elif assembly.location_state == 'none' %}btn-outline-success{% elif assembly.location_state == 'rough' %}btn-outline-warning{% else %}btn-outline-danger{% endif %}"
-                      name="location_state"
-                      value="draft"
-                      title="{% trans "AssemblyTeam__edit_position__tooltip-draft" %}">
-                {% trans "Assembly__location_state-draft" %}
-              </button>
-              <button type="submit"
-                      class="btn btn-sm {% if assembly.location_state == 'rough' %}btn-primary{% elif assembly.location_state == 'draft' %}btn-outline-success{% elif assembly.location_state == 'none' %}btn-outline-danger{% elif assembly.location_state == 'final' %}btn-outline-warning{% else %}btn-outline-primary{% endif %}"
-                      name="location_state"
-                      value="rough"
-                      title="{% trans "AssemblyTeam__edit_position__tooltip-rough" %}">
-                {% trans "Assembly__location_state-rough" %}
-              </button>
-              <button type="submit"
-                      class="btn btn-sm {% if assembly.location_state == 'preview' %}btn-primary{% elif assembly.location_state == 'draft' %}btn-outline-warning{% elif assembly.location_state == 'none' %}btn-outline-danger{% elif assembly.location_state == 'final' %}btn-outline-warning{% else %}btn-outline-primary{% endif %}"
-                      name="location_state"
-                      value="preview"
-                      title="{% trans "AssemblyTeam__edit_position__tooltip-preview" %}">
-                {% trans "Assembly__location_state-preview" %}
-              </button>
-              <button type="submit"
-                      class="btn btn-sm {% if assembly.location_state == 'final' %}btn-primary{% elif assembly.location_state == 'none' %}btn-outline-danger{% elif assembly.location_state == 'draft' %}btn-outline-danger{% else %}btn-outline-primary{% endif %}"
-                      name="location_state"
-                      value="final"
-                      title="{% trans "AssemblyTeam__edit_position__tooltip-final" %}">
-                {% trans "Assembly__location_state-final" %}
-              </button>
+              {% include "backoffice/assemblyteam_editposition_statebuttons.html" with object=assembly %}
             </div>
           </form>
         </div>
diff --git a/src/backoffice/templates/backoffice/assemblyteam_editposition_room.html b/src/backoffice/templates/backoffice/assemblyteam_editposition_room.html
new file mode 100644
index 0000000000000000000000000000000000000000..7ed3706a6013a757730668d606cfd1b23c812423
--- /dev/null
+++ b/src/backoffice/templates/backoffice/assemblyteam_editposition_room.html
@@ -0,0 +1,89 @@
+{% extends "backoffice/base.html" %}
+{% load i18n %}
+{% load static %}
+{% load widget_tweaks %}
+
+{% block content %}
+  {% if form.errors %}
+    <div class="row">
+      <div class="col-md-12">
+        <div class="alert alert-danger">{{ form.errors }}</div>
+      </div>
+    </div>
+  {% endif %}
+
+  <div class="row">
+    <div class="col-md-12">
+      <div class="card mb-3">
+        <div class="card-header">
+          <strong>{% trans "Room__edit__position" %}</strong>:
+          <code>{{ room.slug }}</code>
+          {{ room.name }}
+        </div>
+        <div class="card-body">
+          <form action="{% url 'backoffice:assemblyteam-editposition-room' pk=room.id %}"
+                method="post">
+            {% csrf_token %}
+            <input type="hidden" name="value" value="{{ new_value }}">
+
+            {% if room.blocked %}
+              <div class="alert alert-warning">
+                <i class="bi bi-sign-do-not-enter"></i> {% trans "Room__blocked__help" %}
+              </div>
+            {% endif %}
+
+            {% include "core/map.html" with map_config=conference.map_config.backoffice poi_id="poi" areas_id="areas" floor_id="floor" assembly=room.assembly %}
+
+            <div class="d-{% if debug %}block{% else %}none{% endif %}">
+              <div class="row mb-3">
+                <label class="col-sm-2 col-form-label text-muted" for="floor">{% trans "Room__location_floor" %}</label>
+                <div class="col-sm-10">
+                  <input class="form-control"
+                         readonly
+                         type="text"
+                         name="location_floor"
+                         id="floor"
+                         value="{{ room.get_location_floor_index|default:"" }}">
+                </div>
+              </div>
+              <div class="row mb-3">
+                <label class="col-sm-2 col-form-label text-muted" for="poi">{% trans "Room__location_point" %}</label>
+                <div class="col-sm-10">
+                  <input class="form-control"
+                         readonly
+                         type="text"
+                         name="location_point"
+                         id="poi"
+                         value="{{ map_location|default:"" }}">
+                </div>
+              </div>
+              <div class="row mb-3">
+                <label class="col-sm-2 col-form-label text-muted" for="areas">{% trans "Room__location_boundaries" %}</label>
+                <div class="col-sm-10">
+                  <input class="form-control"
+                         readonly
+                         type="text"
+                         name="location_boundaries"
+                         id="areas"
+                         value="{{ map_boundaries|default:"" }}">
+                </div>
+              </div>
+            </div>
+
+            <p>{% trans "AssemblyTeam__add_comment_field" %}</p>
+            <textarea name="comment"
+                      class="form-control"
+                      placeholder="(optional)"
+                      autocomplete="off"></textarea>
+
+            <label class="col-sm-2 col-form-label text-muted" for="buttons">{% trans "AssemblyTeam__edit_position-save" %}</label>
+            <div class="btn-group" id="buttons">
+              {% include "backoffice/assemblyteam_editposition_statebuttons.html" with object=room %}
+            </div>
+          </form>
+        </div>
+      </div>
+    </div>
+  </div>
+
+{% endblock content %}
diff --git a/src/backoffice/templates/backoffice/assemblyteam_editposition_statebuttons.html b/src/backoffice/templates/backoffice/assemblyteam_editposition_statebuttons.html
new file mode 100644
index 0000000000000000000000000000000000000000..67f0664c614cb76312a5d3413c1368c06382e5e8
--- /dev/null
+++ b/src/backoffice/templates/backoffice/assemblyteam_editposition_statebuttons.html
@@ -0,0 +1,36 @@
+{% load i18n %}
+<button type="submit"
+        class="btn btn-sm {% if object.location_state == 'none' %}btn-primary{% else %}btn-outline-danger{% endif %}"
+        name="location_state"
+        value="none"
+        title="{% trans "AssemblyTeam__edit_position__tooltip-none" %}">
+  {% trans "Assembly__location_state-none" %}
+</button>
+<button type="submit"
+        class="btn btn-sm {% if object.location_state == 'draft' %}btn-primary{% elif object.location_state == 'none' %}btn-outline-success{% elif object.location_state == 'rough' %}btn-outline-warning{% else %}btn-outline-danger{% endif %}"
+        name="location_state"
+        value="draft"
+        title="{% trans "AssemblyTeam__edit_position__tooltip-draft" %}">
+  {% trans "Assembly__location_state-draft" %}
+</button>
+<button type="submit"
+        class="btn btn-sm {% if object.location_state == 'rough' %}btn-primary{% elif object.location_state == 'draft' %}btn-outline-success{% elif object.location_state == 'none' %}btn-outline-danger{% elif object.location_state == 'final' %}btn-outline-warning{% else %}btn-outline-primary{% endif %}"
+        name="location_state"
+        value="rough"
+        title="{% trans "AssemblyTeam__edit_position__tooltip-rough" %}">
+  {% trans "Assembly__location_state-rough" %}
+</button>
+<button type="submit"
+        class="btn btn-sm {% if object.location_state == 'preview' %}btn-primary{% elif object.location_state == 'draft' %}btn-outline-warning{% elif object.location_state == 'none' %}btn-outline-danger{% elif object.location_state == 'final' %}btn-outline-warning{% else %}btn-outline-primary{% endif %}"
+        name="location_state"
+        value="preview"
+        title="{% trans "AssemblyTeam__edit_position__tooltip-preview" %}">
+  {% trans "Assembly__location_state-preview" %}
+</button>
+<button type="submit"
+        class="btn btn-sm {% if object.location_state == 'final' %}btn-primary{% elif object.location_state == 'none' %}btn-outline-danger{% elif object.location_state == 'draft' %}btn-outline-danger{% else %}btn-outline-primary{% endif %}"
+        name="location_state"
+        value="final"
+        title="{% trans "AssemblyTeam__edit_position__tooltip-final" %}">
+  {% trans "Assembly__location_state-final" %}
+</button>
diff --git a/src/backoffice/templates/backoffice/assemblyteam_position_info_card.html b/src/backoffice/templates/backoffice/assemblyteam_position_info_card.html
new file mode 100644
index 0000000000000000000000000000000000000000..00c5e80955387dd5654a9b22e706ec1ac1f908df
--- /dev/null
+++ b/src/backoffice/templates/backoffice/assemblyteam_position_info_card.html
@@ -0,0 +1,72 @@
+{% load i18n %}
+<div class="card mb-3">
+  <div class="card-header">Position</div>
+  <div class="card-body d-flex flex-row">
+    <div>
+      <label for="pos_public">
+        <i class="bi bi-eye{% if not object.is_placed %}-slash{% endif %}"></i> {% trans "AssemblyTeam__position__status" %}:
+      </label>
+      <br>
+      <label for="pos_point">
+        <i class="bi bi-pin-map"></i> {% trans "AssemblyTeam__position__point" %}:
+      </label>
+      <br>
+      <label for="pos_boundary">
+        <i class="bi bi-map"></i> {% trans "AssemblyTeam__position__boundary" %}
+      </label>
+    </div>
+    <div class="flex-grow-1 ps-3">
+      {% if object.location_state == object.LocationState.FINAL %}
+        <span id="pos_public"
+              class="text-success"
+              title="{{ object.get_location_state_display }}"><i class="bi bi-check-square"></i> {% trans "AssemblyTeam__position-final" %}</span>
+      {% elif object.location_state == object.LocationState.PREVIEW %}
+        <span id="pos_public"
+              class="text-success"
+              title="{{ object.get_location_state_display }}"><i class="bi bi-check-square"></i> {% trans "AssemblyTeam__position-preview" %}</span>
+      {% elif object.location_state == object.LocationState.DRAFT %}
+        <span id="pos_public"
+              class="text-warning"
+              title="{{ object.get_location_state_display }}"><i class="bi bi-x-square"></i> {% trans "AssemblyTeam__position-draft" %}</span>
+      {% elif object.location_state == object.LocationState.ROUGH %}
+        <span id="pos_public"
+              class="text-warning"
+              title="{{ object.get_location_state_display }}"><i class="bi bi-x-square"></i> {% trans "AssemblyTeam__position-internal" %}</span>
+      {% else %}
+        <span id="pos_public"
+              class="text-danger"
+              title="{{ object.get_location_state_display }}"><i class="bi bi-x-square"></i> {% trans "AssemblyTeam__position-none" %}</span>
+      {% endif %}
+      <br>
+      {% if object.location_data.point %}
+        <span id="pos_point"
+              class="text-success"
+              title="{{ object.location_data.point }}">
+          <i class="bi bi-check-square"></i> {% trans "yes" %}
+        </span>
+      {% else %}
+        <span id="pos_poi" class="text-danger"><i class="bi bi-x-square"></i> {% trans "no" %}</span>
+      {% endif %}
+      <br>
+      {% if object.location_data.boundaries %}
+        <span id="pos_boundary"
+              class="text-success"
+              title="{{ object.location_data.boundaries }}">
+          <i class="bi bi-check-square"></i> {% trans "yes" %}
+        </span>
+      {% else %}
+        <span id="pos_boundary" class="text-danger"><i class="bi bi-x-square"></i> {% trans "no" %}</span>
+      {% endif %}
+    </div>
+    {% if position_edit_url %}
+      <div>
+        <a role="button"
+           class="btn btn-sm btn-{% if assembly.is_public %}primary{% else %}secondary{% endif %}"
+           href="{{ position_edit_url }}">
+          <i class="bi bi-pencil"></i>
+          {% trans "edit" %}
+        </a>
+      </div>
+    {% endif %}
+  </div>
+</div>
diff --git a/src/backoffice/urls.py b/src/backoffice/urls.py
index c7f1201d865df555db5c553dcea617fb07a4d6c8..ea6b2f8a1d5051027259fef4fc9fa1189f4cc200 100644
--- a/src/backoffice/urls.py
+++ b/src/backoffice/urls.py
@@ -67,8 +67,9 @@ urlpatterns = [
     path('assemblyteam/<uuid:pk>', assemblyteam.AssemblyView.as_view(), name='assemblyteam-detail'),
     path('assemblyteam/<uuid:pk>/state', assemblyteam.AssemblyEditStateView.as_view(), name='assemblyteam-editstate'),
     path('assemblyteam/<uuid:pk>/hierarchy', assemblyteam.AssemblyEditHierarchyView.as_view(), name='assemblyteam-edithierarchy'),
-    path('assemblyteam/<uuid:pk>/position', assemblyteam.AssemblyEditPlacementView.as_view(), name='assemblyteam-editposition'),
+    path('assemblyteam/<uuid:pk>/position', assemblyteam.AssemblyEditPlacementView.as_view(), name='assemblyteam-editposition-assembly'),
     path('assemblyteam/<uuid:pk>/message', assemblyteam.AssemblyMessageView.as_view(), name='assemblyteam-message'),
+    path('assemblyteam/room/<uuid:pk>/position', assemblyteam.EditRoomPositionView.as_view(), name='assemblyteam-editposition-room'),
     path('assembly/create', assemblies.AssemblyCreateView.as_view(), name='assembly-create'),
     path('assembly/<uuid:pk>', assemblies.AssemblyDetailView.as_view(), name='assembly'),
     path('assembly/<uuid:pk>/edit', assemblies.AssemblyUpdateView.as_view(), name='assembly-edit'),
diff --git a/src/backoffice/views/assemblyteam.py b/src/backoffice/views/assemblyteam.py
index e57fc7563cd05a3c025ed524316bf5c47efcbd60..5695da9617ed089cc698d9ab4a0dd801adb9e879 100644
--- a/src/backoffice/views/assemblyteam.py
+++ b/src/backoffice/views/assemblyteam.py
@@ -162,7 +162,7 @@ class AssembliesListMixin(AssemblyTeamMixin, FilteredListView):
         return qs
 
 
-def build_nav_from_assembly(assembly):
+def build_nav_from_assembly(assembly, append_children: dict | None = None):
     def _build_nav_from_assembly(assmbly: Assembly, way_up: bool):
         # for cluster members, go way up to the top and trickle down
         if way_up and assmbly.parent:
@@ -171,12 +171,16 @@ def build_nav_from_assembly(assembly):
         me = {
             'caption': assmbly.slug,
             'link': reverse('backoffice:assemblyteam-detail', kwargs={'pk': assmbly.pk}),
-            'active': assembly.id == assmbly.id,
+            'active': assembly.id == assmbly.id and not append_children,
         }
 
         if assmbly.is_cluster:
             me['children'] = [_build_nav_from_assembly(a, way_up=False) for a in assmbly.children.order_by('slug').all()]
             me['expanded'] = True
+        elif append_children:
+            # we are the last child (as we're not recursing further)
+            me['children'] = append_children
+            me['expanded'] = True
 
         return [me] if way_up else me
 
@@ -639,12 +643,12 @@ class AssemblyEditPlacementView(SingleAssemblyTeamMixin, View):
         except ValueError:
             logger.exception('Failed to update position of assembly %s', assembly.pk)
             messages.error(request, gettext('Assembly__edit__position_error'))
-            return redirect('backoffice:assemblyteam-editposition', pk=assembly.pk)
+            return redirect('backoffice:assemblyteam-editposition-assembly', pk=assembly.pk)
 
         if location_state and location_state != assembly.location_state:
             if location_state != Assembly.LocationState.NONE and not (assembly.location_data.get('point') or assembly.location_data.get('boundaries')):
                 messages.warning(request, gettext('Assembly__edit__position_missing_on_publish'))
-                return redirect('backoffice:assemblyteam-editposition', pk=assembly.pk)
+                return redirect('backoffice:assemblyteam-editposition-assembly', pk=assembly.pk)
 
             assembly.location_state = location_state
             changes['location_state'] = location_state
@@ -680,7 +684,7 @@ class AssemblyEditPlacementView(SingleAssemblyTeamMixin, View):
             messages.warning(request, 'TODO: inform assembly')
 
         # we're done saving, redirect to edit view again
-        return redirect('backoffice:assemblyteam-editposition', pk=assembly.pk)
+        return redirect('backoffice:assemblyteam-editposition-assembly', pk=assembly.pk)
 
     def get(self, *args, **kwargs):
         context = self.get_context_data()
@@ -688,7 +692,123 @@ class AssemblyEditPlacementView(SingleAssemblyTeamMixin, View):
         loc_data = context['assembly'].location_data or {}
         context['map_location'] = loc_data.get('point')  # context['assembly'].get_location_point_as_json()
         context['map_boundaries'] = loc_data.get('boundaries')  # context['assembly'].get_location_boundaries_as_json()
-        return render(self.request, 'backoffice/assemblyteam_editposition.html', context)
+        return render(self.request, 'backoffice/assemblyteam_editposition_assembly.html', context)
+
+
+class EditRoomPositionView(AssemblyTeamMixin, View):
+    def dispatch(self, *args, **kwargs):
+        self.room = self.conference.rooms.get(pk=kwargs['pk'])
+        return super().dispatch(*args, **kwargs)
+
+    def get_context_data(self, *args, **kwargs):
+        ctx = super().get_context_data(*args, **kwargs)
+        ctx['room'] = self.room
+        sidebar_room = {
+            'caption': f'{self.room.get_room_type_display()} {self.room.slug}',
+            'link': reverse('backoffice:assemblyteam-editposition-room', kwargs={'pk': self.room.pk}),
+            'active': True,
+        }
+        ctx['sidebar'] = {
+            'title': _('nav_assemblies'),
+            'title_link': reverse('backoffice:assemblies'),
+            'items': build_nav_from_assembly(self.room.assembly, append_children=[sidebar_room]),
+        }
+        return ctx
+
+    def post(self, *args, **kwargs):
+        request = self.request
+        room = self.room
+
+        location_state = request.POST.get('location_state', None)
+        poi = request.POST.get('location_point', None)
+        boundaries = request.POST.get('location_boundaries', None)
+        floor = request.POST.get('location_floor', None)
+        comment = request.POST.get('comment', '').strip()
+
+        old_values = {
+            'location_state': room.location_state,
+            'location_point': str(room.get_location_point_xy() or ''),
+            'location_boundaries': str(room.get_location_boundaries_xy() or ''),
+            'location_floor': str(room.get_location_floor_index() or ''),
+        }
+        changes = {}
+        try:
+            room.location_data = room.location_data or {}
+
+            if poi and poi != '""':
+                parsed = json.loads(poi)
+                assert isinstance(parsed, list) and len(parsed) == 2
+
+                room.location_data['point'] = parsed
+                changes['location_point'] = poi
+            else:
+                room.location_data['point'] = None
+                changes['location_point'] = ''
+
+            if boundaries and boundaries != '""':
+                parsed = json.loads(boundaries)
+                assert isinstance(parsed, list)
+
+                room.location_data['boundaries'] = parsed
+                changes['location_boundaries'] = boundaries
+            else:
+                room.location_data['boundaries'] = None
+                changes['location_boundaries'] = ''
+
+            if floor and floor != '""':
+                parsed = int(floor)
+
+                room.location_floor = self.conference.map_floors.filter(index=parsed).first()
+                changes['location_floor'] = str(room.get_location_floor_index() or '')
+            else:
+                room.location_floor = None
+                changes['location_floor'] = ''
+
+        except ValueError:
+            logger.exception('Failed to update position of room %s', room.pk)
+            messages.error(request, gettext('Assembly__edit__position_error'))
+            return redirect('backoffice:assemblyteam-editposition-room', pk=room.pk)
+
+        if location_state and location_state != room.location_state:
+            if location_state != Assembly.LocationState.NONE and not (room.location_data.get('point') or room.location_data.get('boundaries')):
+                messages.warning(request, gettext('Assembly__edit__position_missing_on_publish'))
+                return redirect('backoffice:assemblyteam-editposition-room', pk=room.pk)
+
+            room.location_state = location_state
+            changes['location_state'] = location_state
+
+        room.save(update_fields=['location_state', 'location_floor', 'location_data'])
+
+        # log the action
+        messages.success(request, gettext('AssemblyTeam__edit_position__success'))
+        logger.info(
+            'Room "%(room_slug)s" (%(room_pk)s): set location_state to "%(location_state)s, POI to "%(poi)s" & boundaries to "%(boundaries)s" & floor to "%(floor)s" upon request by <%(user)s>.',  # noqa:E501
+            {
+                'room_slug': room.slug,
+                'room_pk': room.pk,
+                'location_state': location_state,
+                'floor': floor,
+                'poi': poi,
+                'boundaries': boundaries,
+                'user': self.request.user.username,
+            },
+        )
+        room.log_activity(
+            user=self.request.user,
+            kind=ActivityLogEntry.Kind.STAFF,
+            comment=comment if comment else None,
+            **{k: ActivityLogChange(old=old_values[k], new=v) for k, v in changes.items() if old_values[k] != v},
+        )
+
+        # we're done saving, redirect to edit view again
+        return redirect('backoffice:assemblyteam-editposition-room', pk=room.pk)
+
+    def get(self, *args, **kwargs):
+        context = self.get_context_data()
+        context['uses_map'] = True
+        context['map_location'] = self.room.get_location_point_as_json()
+        context['map_boundaries'] = self.room.get_location_boundaries_as_json()
+        return render(self.request, 'backoffice/assemblyteam_editposition_room.html', context)
 
 
 class AssemblyMessageView(SingleAssemblyTeamMixin, FormView):
diff --git a/src/core/admin.py b/src/core/admin.py
index c00e2f344b16a5603d58bd58a4a1ca6cb4c48b4d..6f45d562bfa6f2860dea297ee05701d75e0dc1e2 100644
--- a/src/core/admin.py
+++ b/src/core/admin.py
@@ -770,9 +770,9 @@ class RoomShareAdmin(admin.ModelAdmin):
 
 
 class RoomAdmin(admin.ModelAdmin):
-    list_display = ['conference', 'assembly', 'name', 'room_type', 'blocked', 'official_room_order', 'id']
+    list_display = ['conference', 'assembly', 'name', 'room_type', 'location_state', 'blocked', 'official_room_order', 'id']
     list_display_links = ['name']
-    list_filter = ['conference', 'room_type', 'backend_status', 'blocked', 'is_official', 'is_public_fahrplan']
+    list_filter = ['conference', 'room_type', 'backend_status', 'location_state', 'blocked', 'is_official', 'is_public_fahrplan']
     save_as = True
     search_fields = ['assembly__name', 'name', 'slug', 'id']
     inlines = [RoomLinkInline, RoomShareInline, TagsInline]
@@ -792,6 +792,10 @@ class RoomAdmin(admin.ModelAdmin):
             'Data',
             {'fields': ['name', 'room_type', 'capacity', 'occupants', 'description']},
         ),
+        (
+            'Location',
+            {'fields': ['location_state', 'location_data']},
+        ),
         (
             'Backend',
             {'fields': ['backend_link', 'backend_link_branch', 'backend_status', 'backend_data', 'director_data']},
diff --git a/src/core/locale/de/LC_MESSAGES/django.po b/src/core/locale/de/LC_MESSAGES/django.po
index 54605f17f955c3b137a75835b2e6c296b318595e..5f859b6dbd58c8f833ba236dd80a63ccaa6c2602 100644
--- a/src/core/locale/de/LC_MESSAGES/django.po
+++ b/src/core/locale/de/LC_MESSAGES/django.po
@@ -1763,6 +1763,18 @@ msgstr "Art des Raums"
 msgid "Room__type"
 msgstr "Typ"
 
+msgid "Room__location_state__help"
+msgstr "Qualität der ausgewählten Position auf der Karte"
+
+msgid "Room__location_state"
+msgstr "Position (Karte) Status"
+
+msgid "Room__location_floor__help"
+msgstr "Ebene, auf der sich der Raum befindet"
+
+msgid "Room__location_floor"
+msgstr "Ebene"
+
 msgid "Room__backend_link__help"
 msgstr "Referenz zu interner Quelle"
 
diff --git a/src/core/locale/en/LC_MESSAGES/django.po b/src/core/locale/en/LC_MESSAGES/django.po
index d5c6b5e3d29d407e90a308af459a86bdf22ae8f1..a2be9e62b7a53bc5502bfd2de34bbe866affb35d 100644
--- a/src/core/locale/en/LC_MESSAGES/django.po
+++ b/src/core/locale/en/LC_MESSAGES/django.po
@@ -1763,6 +1763,18 @@ msgstr "type of room"
 msgid "Room__type"
 msgstr "type"
 
+msgid "Room__location_state__help"
+msgstr "quality of the selected position, how to handle it?"
+
+msgid "Room__location_state"
+msgstr "location state"
+
+msgid "Room__location_floor__help"
+msgstr "floor, on which this room is placed"
+
+msgid "Room__location_floor"
+msgstr "floor"
+
 msgid "Room__backend_link__help"
 msgstr "reference to internal (backend) source"
 
diff --git a/src/core/migrations/0174_room_location.py b/src/core/migrations/0174_room_location.py
new file mode 100644
index 0000000000000000000000000000000000000000..6b96922c16b62a4f8018c676a3f13394a5201e77
--- /dev/null
+++ b/src/core/migrations/0174_room_location.py
@@ -0,0 +1,29 @@
+# Generated by Django 5.1.3 on 2024-12-26 13:10
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('core', '0173_lock_created_at'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='room',
+            name='location_data',
+            field=models.JSONField(blank=True, null=True),
+        ),
+        migrations.AddField(
+            model_name='room',
+            name='location_floor',
+            field=models.ForeignKey(blank=True, help_text='Room__location_floor__help', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='rooms', to='core.mapfloor', verbose_name='Room__location_floor'),
+        ),
+        migrations.AddField(
+            model_name='room',
+            name='location_state',
+            field=models.CharField(choices=[('none', 'Assembly__location_state-none'), ('draft', 'Assembly__location_state-draft'), ('rough', 'Assembly__location_state-rough'), ('preview', 'Assembly__location_state-preview'), ('final', 'Assembly__location_state-final')], default='none', help_text='Room__location_state__help', max_length=20, verbose_name='Room__location_state'),
+        ),
+    ]
diff --git a/src/core/models/rooms.py b/src/core/models/rooms.py
index cef75f46a77b2b89f46a675ac1a6e2c22217d303..5543aaa592aaf36ed90cf211528029b306de5188 100644
--- a/src/core/models/rooms.py
+++ b/src/core/models/rooms.py
@@ -1,3 +1,4 @@
+import json
 import uuid
 from datetime import datetime
 from pathlib import Path
@@ -15,6 +16,7 @@ from django.utils.translation import gettext_lazy as _
 
 from core.fields import ConferenceReference
 from core.markdown import compile_translated_markdown_fields, store_relationships
+from core.models.activitylog import ActivityLogMixin
 from core.models.assemblies import Assembly
 from core.models.base_managers import ConferenceManagerMixin
 from core.models.conference import Conference, ConferenceMember
@@ -100,7 +102,7 @@ def get_banner_help_test() -> str:
     }
 
 
-class Room(BackendMixin, models.Model):
+class Room(BackendMixin, ActivityLogMixin, models.Model):
     class Meta:
         verbose_name = _('Room')
         verbose_name_plural = _('Rooms')
@@ -153,6 +155,27 @@ class Room(BackendMixin, models.Model):
     room_type = models.CharField(max_length=20, choices=RoomType.choices, help_text=_('Room__type__help'), verbose_name=_('Room__type'))
     """Style of the room."""
 
+    location_state = models.CharField(
+        max_length=20,
+        choices=Assembly.LocationState.choices,
+        default=Assembly.LocationState.NONE,
+        help_text=_('Room__location_state__help'),
+        verbose_name=_('Room__location_state'),
+    )
+    location_floor = models.ForeignKey(
+        'MapFloor',
+        blank=True,
+        null=True,
+        related_name='rooms',
+        on_delete=models.PROTECT,
+        help_text=_('Room__location_floor__help'),
+        verbose_name=_('Room__location_floor'),
+    )
+    location_data = models.JSONField(
+        blank=True,
+        null=True,
+    )
+
     backend_link = models.URLField(blank=True, null=True, help_text=_('Room__backend_link__help'), verbose_name=_('Room__backend_link'))
     """Reference to integration/source system. For e.g. WorkAdventure this is the collection JSON URL."""
 
@@ -301,6 +324,29 @@ class Room(BackendMixin, models.Model):
     def generate_slug(self):
         self.slug = self.__create_slug()
 
+    def get_location_floor_index(self) -> int | None:
+        return self.location_floor.index if self.location_floor else None
+
+    def get_location_point_xy(self) -> tuple[int, int] | None:
+        if self.location_data is None:
+            return None
+        if p := self.location_data.get('point'):
+            return p[0], p[1]
+        return None
+
+    def get_location_point_as_json(self) -> str:
+        return json.dumps(self.get_location_point_xy() or '')
+
+    def get_location_boundaries_xy(self) -> list[list[tuple[int, int]]] | None:
+        if self.location_data is None:
+            return None
+        if b := self.location_data.get('boundaries'):
+            return b
+        return None
+
+    def get_location_boundaries_as_json(self) -> str:
+        return json.dumps(self.get_location_boundaries_xy() or '')
+
     def clean(self):
         if self.room_type == self.RoomType.PROJECT:
             raise ValidationError(_('Room__type_project_not_allowed'))