diff --git a/src/components/shows/Schedules.vue b/src/components/shows/Schedules.vue
index 0fa47e3875c8cd1d8b2cb2eadae134de583496b2..e8192d3d1df622fe86afe3c973f28e9b5c402d63 100644
--- a/src/components/shows/Schedules.vue
+++ b/src/components/shows/Schedules.vue
@@ -1,77 +1,120 @@
 <template>
-  <div
-    v-if="relevantSchedules.length > 0"
-    :class="{
-      'show-schedules': true,
-      expandable: isExpandable,
-      collapsed: isExpandable && isCollapsed,
-    }"
-    @click="isCollapsed = !isCollapsed"
-  >
-    <table class="table b-table table-striped border">
-      <thead>
-        <tr>
-          <th class="top-header">
-            {{ uppercaseFirst(t('showSchedules.schedule')) }}
-          </th>
-          <th class="top-header text-right font-weight-normal">
-            {{ relevantSchedules.length }}
-            {{
-              relevantSchedules.length === 1
-                ? t('showSchedules.schedule')
-                : t('showSchedules.schedules')
-            }}
-          </th>
-        </tr>
-      </thead>
-
-      <tbody>
-        <tr v-for="schedule in relevantSchedules" :key="schedule.id">
-          <td>
-            {{ renderRruleForSchedule(schedule) }}
-          </td>
-          <td class="text-right">
-            <template v-if="schedule.firstDate === schedule.lastDate">
-              {{ prettyDate(parseISO(schedule.firstDate)) }} <br />
-            </template>
-
-            {{ prettyHours(schedule.startTime) }} - {{ prettyHours(schedule.endTime) }}
-          </td>
-        </tr>
-      </tbody>
-    </table>
-
-    <div v-if="isExpandable" class="collapser">
-      {{ t('showSchedules.collapse') }}
-    </div>
-  </div>
-
-  <div v-else class="border p-4 mb-4">
-    {{ t('showSchedules.noSchedulesAvailable') }}
-  </div>
+  <section>
+    <header class="tw-flex tw-items-center tw-gap-6 tw-mt-12 tw-mb-4">
+      <SectionTitle>{{ t('showSchedules.title') }}</SectionTitle>
+
+      <button
+        v-if="collapseSchedules"
+        type="button"
+        class="btn btn-default"
+        aria-controls="schedule-collapsable"
+        :aria-expanded="!isCollapsed"
+        @click="isCollapsed = !isCollapsed"
+      >
+        {{ t(isCollapsed ? 'showSchedules.showAll' : 'showSchedules.hide') }}
+      </button>
+    </header>
+
+    <Collapse
+      id="schedule-collapsable"
+      v-model:is-collapsed="isCollapsed"
+      class="tw-mb-12"
+      peek="10rem"
+    >
+      <div class="aura-table-wrapper tw-mb-0">
+        <table class="aura-table">
+          <thead>
+            <tr>
+              <th scope="col">
+                {{ t('showSchedules.rhythm') }}
+              </th>
+              <th scope="col">
+                {{ t('showSchedules.firstBroadcast') }}
+              </th>
+              <th scope="col">
+                {{ t('showSchedules.lastBroadcast') }}
+              </th>
+              <th scope="col" class="tw-text-right">
+                {{ t('showSchedules.times') }}
+              </th>
+            </tr>
+          </thead>
+
+          <tbody>
+            <tr
+              v-for="schedule in relevantSchedules"
+              :key="schedule.id"
+              :aria-label="
+                t(
+                  schedule.lastDate
+                    ? 'showSchedules.scheduleDescriptionFinite'
+                    : 'showSchedules.scheduleDescription',
+                  {
+                    rhythm: renderRruleForSchedule(schedule),
+                    startDate: prettyDate(parseISO(schedule.firstDate)),
+                    endDate: schedule.lastDate ? prettyDate(parseISO(schedule.lastDate)) : '',
+                    startTime: prettyHours(schedule.startTime),
+                    endTime: prettyHours(schedule.endTime),
+                  },
+                )
+              "
+            >
+              <th scope="row">
+                {{ renderRruleForSchedule(schedule) }}
+              </th>
+              <td :colspan="schedule.firstDate === schedule.lastDate ? 2 : 1">
+                {{ prettyDate(parseISO(schedule.firstDate)) }}
+              </td>
+              <td v-if="schedule.firstDate !== schedule.lastDate">
+                <template v-if="schedule.lastDate">
+                  {{ prettyDate(parseISO(schedule.lastDate)) }}
+                </template>
+                <template v-else> offen </template>
+              </td>
+              <td class="tw-text-right">
+                {{ prettyHours(schedule.startTime) }} - {{ prettyHours(schedule.endTime) }}
+              </td>
+            </tr>
+            <tr v-if="relevantSchedules.length === 0">
+              <td colspan="2">
+                <p>{{ t('showSchedules.noSchedulesAvailable') }}</p>
+              </td>
+            </tr>
+          </tbody>
+        </table>
+      </div>
+    </Collapse>
+  </section>
 </template>
 
-<script setup>
+<script lang="ts" setup>
 import { parseISO } from 'date-fns'
+import { computed, ref, watch } from 'vue'
 import { useStore } from 'vuex'
+
 import { useRRule } from '@/mixins/rrules'
 import { usePretty } from '@/mixins/prettyDate'
 import { lowercaseFirst, uppercaseFirst, useSelectedShow } from '@/utilities'
-import { computed, ref, watch } from 'vue'
 import { useI18n } from '@/i18n'
+import SectionTitle from '@/components/generic/SectionTitle.vue'
+import Collapse from '@/components/generic/Collapse.vue'
+import { Schedule } from '@/types'
+
+defineOptions({
+  compatConfig: { MODE: 3 },
+})
 
 const { t } = useI18n()
 const { rruleRender } = useRRule()
 const { prettyWeekday, prettyDate, prettyHours } = usePretty()
 const store = useStore()
 const selectedShow = useSelectedShow()
-const schedules = computed(() => store.state.shows.schedules)
+const schedules = computed<Schedule[]>(() => store.state.shows.schedules)
 const relevantSchedules = computed(() => schedules.value.filter((s) => !isPossiblyPastSchedule(s)))
-const isExpandable = computed(() => relevantSchedules.value.length > 2)
-
-const isCollapsed = ref(true)
+const collapseSchedules = computed(() => relevantSchedules.value.length > 2)
+const isCollapsed = ref(!collapseSchedules.value)
 
-function isPossiblyPastSchedule(schedule) {
+function isPossiblyPastSchedule(schedule: Schedule) {
   // Recurrence rules get very complex very fast, so we only give
   // a definitive answer if a lastDate is set for this schedule.
   if (!schedule.lastDate) {
@@ -89,7 +132,7 @@ function updateSchedules() {
   }
 }
 
-function renderRruleForSchedule(schedule) {
+function renderRruleForSchedule(schedule: Schedule) {
   if (schedule.rruleId < 3) {
     return uppercaseFirst(rruleRender(schedule.rruleId))
   }
@@ -103,57 +146,3 @@ function renderRruleForSchedule(schedule) {
 // TODO[#127]: this belongs in the store
 watch(selectedShow, updateSchedules, { immediate: true })
 </script>
-
-<script>
-export default {
-  compatConfig: {
-    MODE: 3,
-  },
-}
-</script>
-
-<style scoped>
-.show-schedules {
-  box-sizing: border-box;
-  width: 100%;
-  margin-bottom: 0.5rem;
-  position: relative;
-  overflow-y: hidden;
-}
-
-.show-schedules:hover {
-  cursor: pointer;
-}
-
-.show-schedules.expandable {
-  margin-bottom: 1rem;
-}
-
-.show-schedules.collapsed {
-  height: 10rem;
-}
-
-.show-schedules.collapsed::after {
-  content: 'click to expand';
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  background: linear-gradient(rgba(255, 255, 255, 0), rgba(255, 255, 255, 9) 66%);
-  height: 4rem;
-  width: 100%;
-  z-index: 1;
-  bottom: 0;
-  position: absolute;
-}
-
-.show-schedules.collapsed::after,
-.collapser {
-  text-align: center;
-  color: var(--info);
-}
-
-.show-schedules:hover:after,
-.show-schedules:hover .collapser {
-  text-decoration: underline;
-}
-</style>
diff --git a/src/i18n/de.js b/src/i18n/de.js
index 30d6ad63f90e09a76eb248968e0939902fc15b6f..835934a6ca4ad35092c722537be76a1e817d5797 100644
--- a/src/i18n/de.js
+++ b/src/i18n/de.js
@@ -208,11 +208,18 @@ export default {
   },
 
   showSchedules: {
-    schedule: 'Programm',
-    schedules: 'Programme',
-    noSchedulesAvailable: 'Es gibt für diese Sendereihe noch kein Ausstrahlungs-Schema',
-
-    collapse: 'Zum Verkleinern klicken',
+    title: 'Ausstrahlungsschema',
+    times: 'Sendezeiten',
+    rhythm: 'Rhythmus',
+    firstBroadcast: 'Erste Ausstrahlung',
+    lastBroadcast: 'Letzte Ausstrahlung',
+    noSchedulesAvailable: 'Es gibt für diese Sendereihe noch kein Ausstrahlungsschema',
+    showAll: 'Alles anzeigen',
+    hide: 'Ausblenden',
+    scheduleDescription:
+      '%{rhythm} ab %{startDate} jeweils von %{startTime} Uhr bis %{endTime} Uhr.',
+    scheduleDescriptionFinite:
+      '%{rhythm} ab %{startDate} bis zum %{endDate} jeweils von %{startTime} Uhr bis %{endTime} Uhr.',
   },
 
   showTimeslots: {
diff --git a/src/i18n/en.js b/src/i18n/en.js
index e593d53f84f11634e88668b434fce58e604f6c55..c7a490fab01410e6aa2edd2294d9889106fc80bb 100644
--- a/src/i18n/en.js
+++ b/src/i18n/en.js
@@ -208,11 +208,18 @@ export default {
   },
 
   showSchedules: {
-    schedule: 'schedule',
-    schedules: 'schedules',
+    title: 'Schedules',
+    times: 'Airtime',
+    rhythm: 'Rhythm',
+    firstBroadcast: 'First airing',
+    lastBroadcast: 'Last airing',
     noSchedulesAvailable: 'There are currently no schedules for this show',
-
-    collapse: 'click to collapse',
+    showAll: 'Show All',
+    hide: 'Hide',
+    scheduleDescription:
+      '%{rhythm} beginning %{startDate} from %{startTime} to %{endTime} respectively.',
+    scheduleDescriptionFinite:
+      '%{rhythm} beginning %{startDate} through %{endDate} from %{startTime} to %{endTime} respectively.',
   },
 
   showTimeslots: {
diff --git a/src/types.ts b/src/types.ts
index cf8d029f97dcc695bdbd1da34b0ad5d3ac2ceeb3..a14dc1129546de0aed76f874f5c232bb3490c973 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -19,3 +19,4 @@ export type PaginationData = {
 export type TimeSlot = Required<steeringComponents['schemas']['TimeSlot']>
 export type Show = Required<steeringComponents['schemas']['Show']>
 export type Playlist = Required<tankComponents['schemas']['store.Playlist']>
+export type Schedule = Required<steeringComponents['schemas']['Schedule']>