diff --git a/src/components/CalendarDayView.vue b/src/components/CalendarDayView.vue
index 89726311ee295d6e08487af652ac0c1203385657..75250df486eb2acd08e4c07ed7e844f4ae99ca7f 100644
--- a/src/components/CalendarDayView.vue
+++ b/src/components/CalendarDayView.vue
@@ -1,108 +1,224 @@
 <template>
-  <div class="schedule-panel tw-w-full">
-    <div class="tw-flex tw-items-center tw-justify-between tw-mb-4">
-      <h3>{{ prettyDate(selectedDay) }}</h3>
-
-      <div class="fc fc-direction-ltr">
-        <div class="fc-button-group">
-          <button
-            type="button"
-            class="fc-button fc-prev-button fc-button-primary"
-            @click="emit('changeDay', -1)"
-          >
-            <span class="fc-icon fc-icon-chevron-left"></span>
-          </button>
-
-          <button
-            type="button"
-            class="fc-button fc-next-button fc-button-primary"
-            @click="emit('changeDay', 1)"
-          >
-            <span class="fc-icon fc-icon-chevron-right"></span>
-          </button>
-        </div>
-      </div>
-    </div>
-
-    <div
-      v-for="{
-        timeslot,
-        timeslotDurationInSeconds,
-        playlist,
-        playlistDurationInSeconds,
-        show,
-      } in slotData"
-      :key="timeslot.id"
-      class="timeslot tw-w-full tw-py-2 tw-px-3 tw-rounded hover:tw-bg-gray-200 hover:tw-text-gray-900 tw-cursor-pointer tw-border tw-border-solid tw-border-gray-200 tw-mb-2"
-      :class="{
-        'tw-bg-gray-900 tw-text-white':
-          now >= parseISO(timeslot.start) && now <= parseISO(timeslot.end),
-        'tw-bg-gray-200 tw-text-gray-600 tw-opacity-75': now >= parseISO(timeslot.end),
-      }"
-      @click="onTimeslotClick(timeslot)"
-    >
-      <div class="tw-flex tw-justify-between tw-items-center">
-        <div>
-          <p class="tw-mb-0 tw-leading-tight tw-font-bold">
-            <SafeHTML v-if="show" :html="show.name" sanitize-preset="inline-noninteractive" />
-          </p>
-          <span class="tw-text-sm">
-            {{ prettyTime(timeslot.start) }} - {{ prettyTime(timeslot.end) }}
-            <span v-if="timeslot.repetitionOfId" class="tw-text-gray-400">
-              {{ t('calendar.repetition') }}
-            </span>
-          </span>
-        </div>
+  <div>
+    <div class="schedule-panel tw-w-full">
+      <div class="tw-flex tw-items-center tw-justify-between tw-mb-4">
+        <h3>{{ selectedDay.toLocaleString(locale, { dateStyle: 'long' }) }}</h3>
 
-        <div v-if="(timeslot.playlistId || show?.defaultPlaylistId) && playlist">
-          <p>
-            <strong>{{ t('emissionTable.playlist') }}:</strong>
-            {{ playlist.description }}
-            <span
-              v-if="!timeslot.playlistId && show?.defaultPlaylistId"
-              class="tw-text-xs tw-text-red-500"
+        <div class="fc fc-direction-ltr">
+          <div class="fc-button-group">
+            <button
+              type="button"
+              class="fc-button fc-prev-button fc-button-primary"
+              @click="emit('changeDay', -1)"
             >
-              {{ t('calendar.fallback') }}
-            </span>
-          </p>
-
-          <p v-if="timeslot.playlistId" class="tw-text-sm">
-            <strong>{{ t('emissionTable.duration') }}:</strong>
-            {{ secondsToDurationString(playlistDurationInSeconds ?? timeslotDurationInSeconds) }}
-            <span
-              v-if="
-                playlistDurationInSeconds !== null &&
-                timeslotDurationInSeconds !== playlistDurationInSeconds
-              "
-              class="is-mismatched"
+              <span class="fc-icon fc-icon-chevron-left"></span>
+            </button>
+
+            <button
+              type="button"
+              class="fc-button fc-next-button fc-button-primary"
+              @click="emit('changeDay', 1)"
             >
-              {{ t('calendar.mismatchedLength') }}
-            </span>
-          </p>
+              <span class="fc-icon fc-icon-chevron-right"></span>
+            </button>
+          </div>
         </div>
-
-        <p v-else>
-          {{ t('calendar.empty') }}
-        </p>
       </div>
+
+      <table class="slot-table tw-block md:tw-table" role="table">
+        <colgroup>
+          <col width="1%" />
+        </colgroup>
+        <tbody role="rowgroup" class="tw-block md:tw-table-row-group">
+          <template v-for="(slot, index) in slots" :key="index">
+            <tr v-if="index !== 0" role="presentation" class="tw-block md:tw-table-row">
+              <td colspan="2" class="tw-p-0 tw-block md:tw-table-cell tw-w-full">
+                <hr class="tw-my-4" />
+              </td>
+            </tr>
+            <tr
+              class="slot tw-block md:tw-table-row tw-w-full tw-relative"
+              role="row"
+              :class="{
+                'slot--today': nowDateString === selectedDayISODate,
+                'slot--past': now > slot.end,
+                'slot--current': now > slot.start && now < slot.end,
+                'slot--future': now < slot.start,
+              }"
+            >
+              <th
+                class="slot-time tw-block md:tw-table-cell tw-align-top tw-font-normal tw-text-gray-600"
+                scope="row"
+                role="rowheader"
+              >
+                <span v-if="now > slot.start && now < slot.end" class="slot-time-indicator">
+                  <span
+                    :style="{
+                      top: `${mapToDomain(
+                        now.getTime(),
+                        [slot.start.getTime(), slot.end.getTime()],
+                        [0, 100],
+                      )}%`,
+                    }"
+                  />
+                </span>
+
+                <span class="tw-whitespace-nowrap">
+                  {{ formatTime(slot.start) }} - {{ formatTime(slot.end) }}
+                </span>
+                <br />
+                <span class="tw-text-sm">
+                  {{ secondsToDurationString(slot.durationInSeconds) }}
+                </span>
+              </th>
+              <td
+                role="cell"
+                class="slot-data tw-block lg-tw-table-cell tw-w-full tw-align-top tw-rounded"
+                :class="{
+                  'tw-bg-gray-50': slot.type === 'show',
+                  'tw-bg-amber-50': slot.type === 'intermission',
+                }"
+              >
+                <template v-if="slot.type === 'intermission'">
+                  {{ t('calendar.intermission') }}
+                </template>
+
+                <div v-if="slot.type === 'show'" class="tw-flex tw-flex-col tw-gap-2">
+                  <div>
+                    <div class="tw-float-right">
+                      <button
+                        v-if="slot.timeslot.showId === selectedShow.id"
+                        type="button"
+                        class="btn btn-sm btn-default"
+                        :title="t('calendar.editTimeslot')"
+                        @click="editTimeslot(slot.timeslot)"
+                      >
+                        <icon-system-uicons-pen />
+                      </button>
+
+                      <button
+                        v-if="slot.timeslot.showId !== selectedShow.id"
+                        class="btn btn-sm btn-default"
+                        :title="
+                          t('calendar.switchShow', { show: sanitizeHTML(slot.show?.name ?? '') })
+                        "
+                        @click="switchShow(slot.timeslot)"
+                      >
+                        <icon-system-uicons-reverse />
+                      </button>
+                    </div>
+                    <SafeHTML
+                      :html="slot.show?.name ?? ''"
+                      sanitize-preset="inline-noninteractive"
+                      as="p"
+                      class="tw-font-bold tw-m-0"
+                    />
+                  </div>
+
+                  <div
+                    class="tw-grid tw-gap-1 tw-text-sm empty:tw-hidden"
+                    style="grid-template-columns: max-content minmax(0, 1fr)"
+                  >
+                    <template
+                      v-if="
+                        (slot.timeslot.playlistId || slot.show?.defaultPlaylistId) && slot.playlist
+                      "
+                    >
+                      <span>{{ t('emissionTable.playlist') }}:</span>
+                      <span>{{ slot.playlist.description }}</span>
+                    </template>
+                    <template v-if="slot.playlistDurationInSeconds">
+                      <span>{{ t('emissionTable.duration') }}:</span>
+                      <span>{{ secondsToDurationString(slot.playlistDurationInSeconds) }}</span>
+                    </template>
+                  </div>
+
+                  <div class="tw-flex tw-gap-2">
+                    <span
+                      v-if="now > slot.start && now < slot.end"
+                      :class="pillClasses"
+                      class="tw-bg-teal-200"
+                    >
+                      {{ t('calendar.playing') }}
+                    </span>
+
+                    <span
+                      v-if="slot.timeslot.repetitionOfId"
+                      :class="pillClasses"
+                      class="tw-bg-amber-200"
+                    >
+                      {{ t('calendar.repetition') }}
+                    </span>
+                    <span
+                      v-if="
+                        slot.playlistDurationInSeconds !== null &&
+                        slot.timeslotDurationInSeconds !== slot.playlistDurationInSeconds
+                      "
+                      :class="pillClasses"
+                      class="tw-bg-amber-200"
+                    >
+                      {{ t('calendar.mismatchedLength') }}
+                    </span>
+                    <span
+                      v-if="!slot.timeslot.playlistId && slot.show?.defaultPlaylistId"
+                      :class="pillClasses"
+                      class="tw-bg-rose-200"
+                    >
+                      {{ t('calendar.fallback') }}
+                    </span>
+                    <span
+                      v-else-if="!slot.timeslot.playlistId && !slot.show?.defaultPlaylistId"
+                      :class="pillClasses"
+                      class="tw-bg-rose-200"
+                    >
+                      {{ t('calendar.empty') }}
+                    </span>
+                  </div>
+                </div>
+              </td>
+            </tr>
+          </template>
+        </tbody>
+      </table>
     </div>
   </div>
 </template>
 
 <script lang="ts" setup>
-import SafeHTML from '@/components/generic/SafeHTML'
-import { usePretty } from '@/mixins/prettyDate'
-import { useI18n } from '@/i18n'
+import { endOfDay, parseISO, startOfDay } from 'date-fns'
+import { sortBy } from 'lodash'
+import { useNow } from '@vueuse/core'
 import { computed } from 'vue'
-import { getISODateString, useSelectedShow } from '@/utilities'
 import { useStore } from 'vuex'
+
+import SafeHTML from '@/components/generic/SafeHTML'
+import { useI18n } from '@/i18n'
 import { Playlist, Show, TimeSlot } from '@/types'
-import { parseISO } from 'date-fns'
 import { calculatePlaylistDurationInSeconds, usePlaylistStore } from '@/stores/playlists'
-import { calculateDurationSeconds, secondsToDurationString } from '@/util'
-import { useNow } from '@vueuse/core'
+import { getISODateString } from '@/utilities'
+import {
+  calculateDurationSeconds,
+  mapToDomain,
+  sanitizeHTML,
+  secondsToDurationString,
+  useFormattedISODate,
+  useSelectedShow,
+} from '@/util'
+
+const pillClasses = 'tw-py-1 tw-px-2 tw-text-xs tw-rounded-full'
 
-type SlotData = {
+type BaseSlot = {
+  start: Date
+  end: Date
+  durationInSeconds: number
+}
+
+type IntermissionSlot = BaseSlot & {
+  type: 'intermission'
+}
+
+type ShowSlot = BaseSlot & {
+  type: 'show'
   timeslot: TimeSlot
   timeslotDurationInSeconds: number
   show: Show | undefined
@@ -110,6 +226,8 @@ type SlotData = {
   playlistDurationInSeconds: number | null
 }
 
+type Slot = IntermissionSlot | ShowSlot
+
 const props = defineProps<{
   selectedDay: Date
 }>()
@@ -117,50 +235,142 @@ const emit = defineEmits<{
   changeDay: [offset: number]
   editTimeslot: [timeslot: TimeSlot]
 }>()
-const { t } = useI18n()
-const { prettyDate, prettyTime } = usePretty()
-const now = useNow({ interval: 60_000 })
+const { t, locale } = useI18n()
 const store = useStore()
 const selectedShow = useSelectedShow()
+const { itemMap: playlistMap, retrieve: retrievePlaylist } = usePlaylistStore()
 const shows = computed<Show[]>(() => store.state.shows.shows)
 const timeslots = computed<TimeSlot[]>(() => store.state.shows.timeslots)
-const { itemMap: playlistMap, retrieve: retrievePlaylist } = usePlaylistStore()
+const now = useNow({ interval: 60_000 })
+const nowDateString = useFormattedISODate(now)
+const startOfSelectedDay = computed(() => startOfDay(props.selectedDay))
+const endOfSelectedDay = computed(() => endOfDay(props.selectedDay))
 const selectedDayISODate = computed(() => getISODateString(props.selectedDay))
-const slotData = computed<SlotData[]>(() => {
-  const result: SlotData[] = []
-  const daySlots = timeslots.value.filter(
-    (timeslot) => timeslot.start.split('T')[0] === selectedDayISODate.value,
+const slots = computed<Slot[]>(() => {
+  const result: Slot[] = []
+  let start = new Date(startOfSelectedDay.value)
+  const endOfToday = new Date(endOfSelectedDay.value)
+  const daySlots = sortBy(
+    timeslots.value.filter((timeslot) =>
+      // we want either the start date or the end date to be on the currently selected day
+      // so that timeslots that wrap around days are included
+      [
+        extractDateFromDateTimeString(timeslot.start),
+        extractDateFromDateTimeString(timeslot.end),
+      ].includes(selectedDayISODate.value),
+    ),
+    (timeslot) => timeslot.start,
   )
 
   for (const timeslot of daySlots) {
-    const show = shows.value.find(({ id }) => timeslot.showId === id)
-    const playlistId = timeslot.playlistId ?? show?.defaultPlaylistId
-    // enqueue a request for the playlist in case it’s not yet in the store
-    if (playlistId) retrievePlaylist(playlistId, undefined, { useCached: true })
-    const playlist = playlistId ? playlistMap.get(playlistId) : undefined
-    const timeslotDurationInSeconds = calculateDurationSeconds(
-      parseISO(timeslot.start),
-      parseISO(timeslot.end),
-    )
-    const playlistDurationInSeconds = playlist ? calculatePlaylistDurationInSeconds(playlist) : null
-
-    result.push({
-      timeslot,
-      timeslotDurationInSeconds,
-      playlist,
-      playlistDurationInSeconds,
-      show,
-    })
+    const showSlot = createShowSlot(timeslot)
+    if (showSlot.start > start) {
+      // this slot starts some time after the previous slot ended
+      // that means we got an intermission between the last timeslot and the current one
+      result.push(createIntermissionSlot(start, showSlot.start))
+    }
+    start = showSlot.end
+    result.push(showSlot)
+  }
+
+  if (start < endOfToday) {
+    // the last timeslot of this day ended before the end of the day
+    // that means we got an intermission between the end of the last show and the end of the selected day
+    result.push(createIntermissionSlot(start, endOfToday))
   }
 
   return result
 })
 
-function onTimeslotClick(timeslot: TimeSlot) {
-  if (selectedShow.value.id !== timeslot.showId) {
-    selectedShow.value = { id: timeslot.showId } as Show
-  } else {
-    emit('editTimeslot', timeslot)
+function extractDateFromDateTimeString(date: string) {
+  return date.split('T')[0]
+}
+
+function createIntermissionSlot(start: Date, end: Date): IntermissionSlot {
+  return {
+    type: 'intermission',
+    start,
+    end,
+    durationInSeconds: calculateDurationSeconds(start, end),
   }
 }
+
+function createShowSlot(timeslot: TimeSlot): ShowSlot {
+  const show = shows.value.find(({ id }) => timeslot.showId === id)
+  const playlistId = timeslot.playlistId ?? show?.defaultPlaylistId
+  // enqueue a request for the playlist in case it’s not yet in the store
+  if (playlistId) retrievePlaylist(playlistId, undefined, { useCached: true })
+  const playlist = playlistId ? playlistMap.get(playlistId) : undefined
+  const start = parseISO(timeslot.start)
+  const end = parseISO(timeslot.end)
+  const timeslotDurationInSeconds = calculateDurationSeconds(start, end)
+  const playlistDurationInSeconds = playlist ? calculatePlaylistDurationInSeconds(playlist) : null
+  return {
+    type: 'show',
+    start,
+    end,
+    durationInSeconds: calculateDurationSeconds(start, end),
+    timeslot,
+    timeslotDurationInSeconds,
+    playlist,
+    playlistDurationInSeconds,
+    show,
+  }
+}
+
+function formatTime(date: Date) {
+  if (date < startOfSelectedDay.value || date > endOfSelectedDay.value) {
+    return date.toLocaleString(locale.value, {
+      dateStyle: 'short',
+      timeStyle: 'short',
+    })
+  }
+  return date.toLocaleString(locale.value, {
+    timeStyle: 'short',
+  })
+}
+
+function switchShow(timeslot: TimeSlot) {
+  selectedShow.value = { id: timeslot.showId } as Show
+}
+
+function editTimeslot(timeslot: TimeSlot) {
+  emit('editTimeslot', timeslot)
+}
 </script>
+
+<style lang="postcss" scoped>
+.slot-table {
+  width: min(800px, 100%);
+}
+
+.slot-table :is(th, td) {
+  @apply tw-p-2;
+}
+
+.slot-table th {
+  @apply tw-pr-4;
+}
+
+.slot.slot--today.slot--past {
+  @apply tw-opacity-75;
+}
+
+.slot-time-indicator {
+  position: absolute;
+  top: 0;
+  bottom: 0;
+  right: 100%;
+  width: 0.35rem;
+  border: solid theme('colors.gray.300');
+  border-width: 1px 1px 1px 0;
+
+  & > span {
+    position: absolute;
+    right: 0;
+    width: 0.5rem;
+    height: 1px;
+    background-color: var(--fc-now-indicator-color);
+  }
+}
+</style>
diff --git a/src/i18n/de.js b/src/i18n/de.js
index c579df6484ecbd967ddc9a0ebc0a2eac9abb0766..30d6ad63f90e09a76eb248968e0939902fc15b6f 100644
--- a/src/i18n/de.js
+++ b/src/i18n/de.js
@@ -468,14 +468,18 @@ export default {
 
   calendar: {
     today: 'Heute',
-    empty: '(Keine Playlist!)',
-    fallback: 'Achtung! Fallback-Playlist!',
-    repetition: '(Wiederholung)',
+    empty: 'Keine Playlist',
+    fallback: 'Fallback-Playlist',
+    repetition: 'Wiederholung',
     view: {
       day: 'Tagesansicht',
       week: 'Wochenansicht',
     },
-    mismatchedLength: '(Unpassende Länge)',
+    switchShow: 'Zur „%{show}“ Sendereihe wechseln',
+    editTimeslot: 'Sendung bearbeiten',
+    intermission: 'Programmunterbrechung',
+    mismatchedLength: 'Unpassende Länge',
+    playing: 'Spielt gerade',
   },
 
   // Etc
diff --git a/src/i18n/en.js b/src/i18n/en.js
index 672a92daa91c944db2af424e465302a41d7592ab..e593d53f84f11634e88668b434fce58e604f6c55 100644
--- a/src/i18n/en.js
+++ b/src/i18n/en.js
@@ -459,14 +459,18 @@ export default {
 
   calendar: {
     today: 'Today',
-    empty: '(No playlist! Station fallback will be used)',
-    fallback: 'Warning! Fallback playlist!',
-    repetition: '(Repetition)',
+    empty: 'No playlist',
+    fallback: 'Fallback playlist',
+    repetition: 'Repetition',
     view: {
       day: 'Day view',
       week: 'Week view',
     },
-    mismatchedLength: '(wrong duration)',
+    switchShow: 'Switch to “%{show}” show',
+    editTimeslot: 'Sendung bearbeiten',
+    intermission: 'Program intermission',
+    mismatchedLength: 'wrong duration',
+    playing: 'Currently playing',
   },
 
   // Etc