diff --git a/src/pages/courses.tsx b/src/pages/courses.tsx
index cfc33fef79e9a1ec016081b91de05d7026f58c06..da60016cb83d8432fe5fee3e9976368c6021cbc1 100644
--- a/src/pages/courses.tsx
+++ b/src/pages/courses.tsx
@@ -8,7 +8,7 @@ import { useApi } from "@/api";
 import { useAuthStatus } from "@/authentication";
 import { ErrorComponent, FallbackErrorBoundary } from "@/error";
 import { useLanguage } from "@/localization";
-import { ReloadBoundary, semesterToHuman, UpdateOverlay } from "@/miscellaneous";
+import { CollapsableCard, ReloadBoundary, semesterToHuman, UpdateOverlay } from "@/miscellaneous";
 import { useEditMode, OMCreate, OMEdit, EmbeddedOMFieldComponent } from "@/object_management";
 
 type GroupByTypes = "semester" | "full_name" | "organizer" | "topic";
@@ -85,39 +85,36 @@ function CourseGroupCard({
     g,
     groupedby,
     onlyShowPublicCourses,
+    defaultExpanded,
 }: {
     g: { groupTitle: string; list: Array<course> };
     groupedby: string;
     onlyShowPublicCourses: boolean;
+    defaultExpanded: boolean;
 }) {
     return (
-        <div className="card mb-3">
-            <div className="card-body">
-                <h4 className="card-title">{g.groupTitle}</h4>
-                <div className="card-text">
-                    <ul className="courses-list m-0 p-0">
-                        {g.list.flatMap((c) => {
-                            if (
-                                onlyShowPublicCourses &&
-                                (!c.default_authentication_methods?.includes("public") ||
-                                    c.visible === false)
-                            ) {
-                                // I chose not to check c.listed here because
-                                // it may be useful for videoag admins to check which courses are falsely set to visible and public
-                                return [];
-                            }
-                            return [
-                                <CourseItem
-                                    key={c.id}
-                                    course={c}
-                                    showSemester={groupedby !== "semester"}
-                                />,
-                            ];
-                        })}
-                    </ul>
-                </div>
-            </div>
-        </div>
+        <CollapsableCard header={g.groupTitle} defaultVisible={defaultExpanded}>
+            <ul className="courses-list m-0 p-0">
+                {g.list.flatMap((c) => {
+                    if (
+                        onlyShowPublicCourses &&
+                        (!c.default_authentication_methods?.includes("public") ||
+                            c.visible === false)
+                    ) {
+                        // I chose not to check c.listed here because
+                        // it may be useful for videoag admins to check which courses are falsely set to visible and public
+                        return [];
+                    }
+                    return [
+                        <CourseItem
+                            key={c.id}
+                            course={c}
+                            showSemester={groupedby !== "semester"}
+                        />,
+                    ];
+                })}
+            </ul>
+        </CollapsableCard>
     );
 }
 
@@ -154,13 +151,17 @@ function CourseList({
             // descending order for semesters
             return groupBy === "semester" ? -cmp : cmp;
         })
-        .map(([key, groupObject]) => {
+        .map(([key, groupObject], index) => {
+            console.log(index);
             return (
                 <CourseGroupCard
                     key={key}
                     g={groupObject}
                     groupedby={groupBy}
                     onlyShowPublicCourses={onlyShowPublicCourses}
+                    defaultExpanded={
+                        groupBy !== "semester" || key !== "none" /* Collapse 'Zeitlos' by default */
+                    }
                 />
             );
         });
diff --git a/src/pages/index.tsx b/src/pages/index.tsx
index f95c6e9fbd8e9e84ae36423dcb17a011405ad60d..488e59b5c34bf88d9848e5a3b50f798e36f80418 100644
--- a/src/pages/index.tsx
+++ b/src/pages/index.tsx
@@ -10,6 +10,7 @@ import { LectureCard, LectureLiveLabel } from "@/course";
 import { ErrorComponent, showError, showWarningToast, FallbackErrorBoundary } from "@/error";
 import { useLanguage } from "@/localization";
 import {
+    Card,
     datetimeToStringOnlyDate,
     datetimeToStringOnlyTime,
     ReloadBoundary,
@@ -56,10 +57,11 @@ function FeatureCard({ data, all_featured }: { data: featured; all_featured: fea
     }
 
     return (
-        <div className={"card mb-3 " + bgColor}>
-            <div className="card-body">
-                <h5 className="card-title d-flex align-items-center">
-                    <div className="overflow-hidden pb-1">
+        <Card
+            objectVisible={data.visible}
+            header={
+                <>
+                    <span className="overflow-hidden">
                         <EmbeddedOMFieldComponent
                             object_type="featured"
                             object_id={data.id!}
@@ -68,7 +70,7 @@ function FeatureCard({ data, all_featured }: { data: featured; all_featured: fea
                             initialValue={data.title}
                             allowMarkdown={true}
                         />
-                    </div>
+                    </span>
                     {editMode && (
                         <EmbeddedOMFieldComponent
                             object_type="featured"
@@ -78,13 +80,13 @@ function FeatureCard({ data, all_featured }: { data: featured; all_featured: fea
                             initialValue={data.visible}
                         />
                     )}
-                    <div className="flex-fill" />
+                    <span className="flex-fill" />
 
                     {editMode && (
-                        <div>
+                        <span>
                             <OMHistory object_type="featured" object_id={data.id!} />
 
-                            <div className="btn-group m-1" role="group">
+                            <span className="btn-group m-1" role="group">
                                 <button
                                     type="button"
                                     className="btn btn-outline-info"
@@ -101,45 +103,36 @@ function FeatureCard({ data, all_featured }: { data: featured; all_featured: fea
                                 >
                                     <i className="bi bi-arrow-down" />
                                 </button>
-                            </div>
+                            </span>
 
                             <OMEdit object_type="featured" object_id={data.id!} />
                             <OMDelete object_type="featured" object_id={data.id!} />
-                        </div>
+                        </span>
                     )}
-                </h5>
-
-                {data.type === "image" && data.image_url && (
-                    <img
-                        src={data.image_url}
-                        alt={data.title}
-                        className="img-fluid rounded"
-                        style={{ maxHeight: "500px" }}
-                    />
-                )}
-
-                {/* Add support for type lecture and course */}
-
-                <EmbeddedOMFieldComponent
-                    object_type="featured"
-                    object_id={data.id!}
-                    field_id="text"
-                    field_type="string"
-                    initialValue={data.text}
-                    className="flex-fill"
-                    allowMarkdown={true}
+                </>
+            }
+        >
+            {data.type === "image" && data.image_url && (
+                <img
+                    src={data.image_url}
+                    alt={data.title}
+                    className="img-fluid rounded"
+                    style={{ maxHeight: "500px" }}
                 />
-
-                {data.visible === false && (
-                    <>
-                        <hr />
-                        <small className="text-muted">
-                            {language.get("ui.generic.object_invisible")}
-                        </small>
-                    </>
-                )}
-            </div>
-        </div>
+            )}
+
+            {/* Add support for type lecture and course */}
+
+            <EmbeddedOMFieldComponent
+                object_type="featured"
+                object_id={data.id!}
+                field_id="text"
+                field_type="string"
+                initialValue={data.text}
+                className="flex-fill"
+                allowMarkdown={true}
+            />
+        </Card>
     );
 }
 
@@ -301,13 +294,13 @@ export default function Home() {
                         <Search className="mt-3" queryCallback={updateQueryDeferred} />
                     </div>
                 </div>
-                <hr />
                 {query.length > 0 && (
                     <>
-                        <SearchResults query={query} />
                         <hr />
+                        <SearchResults query={query} />
                     </>
                 )}
+                <hr className="mb-0" />
                 {isPending && <div className="spinner-border" />}
                 {homepageData && (
                     <div className="row">
@@ -316,24 +309,12 @@ export default function Home() {
                             <FeaturedContent homepageData={homepageData} />
                         </div>
                         <div className="col-md-6">
-                            <div className="card mb-3">
-                                <div className="card-body">
-                                    <h5 className="card-title">
-                                        {language.get("ui.index.next_recordings")}
-                                    </h5>
-
-                                    <UpcomingUploads homepageData={homepageData} />
-                                </div>
-                            </div>
-                            <div className="card mb-3">
-                                <div className="card-body">
-                                    <h5 className="card-title">
-                                        {language.get("ui.index.recent_videos")}
-                                    </h5>
-
-                                    <RecentUploads homepageData={homepageData} />
-                                </div>
-                            </div>
+                            <Card header={language.get("ui.index.next_recordings")}>
+                                <UpcomingUploads homepageData={homepageData} />
+                            </Card>
+                            <Card header={language.get("ui.index.recent_videos")}>
+                                <RecentUploads homepageData={homepageData} />
+                            </Card>
                         </div>
                     </div>
                 )}
diff --git a/src/pages/internal/changelog.tsx b/src/pages/internal/changelog.tsx
index a22dd121958eb8224e5302771c3cbe2e3b7d7120..e3aef207569f2b355d1c8e376d5c170919d790b0 100644
--- a/src/pages/internal/changelog.tsx
+++ b/src/pages/internal/changelog.tsx
@@ -6,7 +6,7 @@ import { useApi, OBJECT_TYPES } from "@/api";
 import { ModeratorBarrier } from "@/authentication";
 import { showError, ErrorComponent } from "@/error";
 import { ChooserEditor, StringEditor, IntEditor } from "@/form";
-import { ReloadBoundary, useReloadBoundary } from "@/miscellaneous";
+import { Card, ReloadBoundary, useReloadBoundary } from "@/miscellaneous";
 import { useFilteredDataView, PagingNavigation, FilterInput } from "@/object_management";
 
 function ValToStr({ val }: { val: any }) {
@@ -455,63 +455,60 @@ function ChangelogImpl() {
             }),
     );
     return (
-        <div className="card">
-            <div className="card-header d-flex">Changelog</div>
-            <div className="card-body">
-                <p>
-                    Hier werden alle Änderungen an Kursen/Veranstaltungen/Videos etc. geloggt und
-                    können Rückgängig gemacht werden.
-                </p>
-                <ReloadBoundary reloadFunc={reloadData}>
-                    <div className="d-flex">
-                        <FilterInput
-                            component={(props) => (
-                                <ChooserEditor
-                                    langKeyPrefix="object"
-                                    unchosenLangKey="ui.generic.filter.any"
-                                    possibleValues={OBJECT_TYPES}
-                                    {...props}
-                                />
-                            )}
-                            label="Object Type"
-                            filterId="object_type"
-                            setFilter={setFilter}
-                            filters={filters}
-                        />
-                        <FilterInput
-                            component={(props) => <IntEditor allowUndefined={true} {...props} />}
-                            label="Object Id"
-                            filterId="object_id"
-                            setFilter={setFilter}
-                            filters={filters}
-                        />
-                        {/*TODO suggestions? handle errors? handle no object type*/}
-                        <FilterInput
-                            component={(props) => (
-                                <StringEditor maxLength={100} allowUndefined={true} {...props} />
-                            )}
-                            label="Field"
-                            filterId="field_id"
-                            setFilter={setFilter}
-                            filters={filters}
-                        />
-                    </div>
-                    <div className="d-flex">
-                        <PagingNavigation
-                            setFilter={setFilter}
-                            filters={filters}
-                            pageCount={data?.page_count}
-                        />
-                        {isLoading && <div className="spinner-border ms-3 mt-1" />}
-                    </div>
-                    {error ? (
-                        <ErrorComponent error={error} objectName="Changelog" showButtons={false} />
-                    ) : (
-                        data && <ChangelogList changelog={data} setFilter={setFilter} />
-                    )}
-                </ReloadBoundary>
-            </div>
-        </div>
+        <Card header="Changelog">
+            <p>
+                Hier werden alle Änderungen an Kursen/Veranstaltungen/Videos etc. geloggt und können
+                Rückgängig gemacht werden.
+            </p>
+            <ReloadBoundary reloadFunc={reloadData}>
+                <div className="d-flex">
+                    <FilterInput
+                        component={(props) => (
+                            <ChooserEditor
+                                langKeyPrefix="object"
+                                unchosenLangKey="ui.generic.filter.any"
+                                possibleValues={OBJECT_TYPES}
+                                {...props}
+                            />
+                        )}
+                        label="Object Type"
+                        filterId="object_type"
+                        setFilter={setFilter}
+                        filters={filters}
+                    />
+                    <FilterInput
+                        component={(props) => <IntEditor allowUndefined={true} {...props} />}
+                        label="Object Id"
+                        filterId="object_id"
+                        setFilter={setFilter}
+                        filters={filters}
+                    />
+                    {/*TODO suggestions? handle errors? handle no object type*/}
+                    <FilterInput
+                        component={(props) => (
+                            <StringEditor maxLength={100} allowUndefined={true} {...props} />
+                        )}
+                        label="Field"
+                        filterId="field_id"
+                        setFilter={setFilter}
+                        filters={filters}
+                    />
+                </div>
+                <div className="d-flex">
+                    <PagingNavigation
+                        setFilter={setFilter}
+                        filters={filters}
+                        pageCount={data?.page_count}
+                    />
+                    {isLoading && <div className="spinner-border ms-3 mt-1" />}
+                </div>
+                {error ? (
+                    <ErrorComponent error={error} objectName="Changelog" showButtons={false} />
+                ) : (
+                    data && <ChangelogList changelog={data} setFilter={setFilter} />
+                )}
+            </ReloadBoundary>
+        </Card>
     );
 }
 
diff --git a/src/pages/internal/import.tsx b/src/pages/internal/import.tsx
index 15f0f836c8db7db143123dbbb9149d4d5efb0c05..96daebb07c8cfa622d1a2b52f7506774efc8a8f2 100644
--- a/src/pages/internal/import.tsx
+++ b/src/pages/internal/import.tsx
@@ -7,7 +7,7 @@ import type { GetNewConfigurationResponse, course } from "@/api/types";
 import { useApi } from "@/api";
 import { ModeratorBarrier } from "@/authentication";
 import { showError, showErrorToast } from "@/error";
-import { ICalEvent, parseICal, ReloadBoundary, useReloadBoundary } from "@/miscellaneous";
+import { Card, ICalEvent, parseICal, ReloadBoundary, useReloadBoundary } from "@/miscellaneous";
 
 function TerminBody({
     course,
@@ -260,106 +260,107 @@ function TerminBody({
                 <div className="spinner-border mb-2" role="status" />
             ) : null}
 
-            <div className="card mb-2">
-                <div className="card-header d-flex align-items-center">
-                    <span className="flex-grow-1">Im Import, nicht bei uns</span>
-
-                    <button
-                        className="btn btn-primary"
-                        onClick={importAllEvents}
-                        disabled={lockedIds === undefined || lockedIds.length > 0}
-                    >
-                        Alle anlegen
-                    </button>
-                </div>
-                <div className="card-body">
-                    <table className="table">
-                        <thead>
-                            <tr>
-                                <th>Zeit</th>
-                                <th>Ort</th>
-                                <th>Dauer</th>
-                                <th></th>
+            <Card
+                header={
+                    <>
+                        <span className="flex-grow-1">Im Import, nicht bei uns</span>
+                        <button
+                            className="btn btn-primary"
+                            onClick={importAllEvents}
+                            disabled={lockedIds === undefined || lockedIds.length > 0}
+                        >
+                            Alle anlegen
+                        </button>
+                    </>
+                }
+            >
+                <table className="table">
+                    <thead>
+                        <tr>
+                            <th>Zeit</th>
+                            <th>Ort</th>
+                            <th>Dauer</th>
+                            <th></th>
+                        </tr>
+                    </thead>
+                    <tbody>
+                        {new_events.map((event, index) => (
+                            <tr key={index}>
+                                <td>
+                                    {event.startDate
+                                        ? event.startDate.toFormat("yyyy-MM-dd HH:mm:ss")
+                                        : "kein datum"}
+                                </td>
+                                <td>{event.location}</td>
+                                <td>{event.duration}</td>
+                                <td>
+                                    <button
+                                        className="btn btn-primary"
+                                        onClick={() => importEvent(event)}
+                                        disabled={
+                                            lockedIds === undefined ||
+                                            lockedIds.includes(event.locally_unique_id)
+                                        }
+                                    >
+                                        Anlegen
+                                    </button>
+                                </td>
                             </tr>
-                        </thead>
-                        <tbody>
-                            {new_events.map((event, index) => (
-                                <tr key={index}>
-                                    <td>
-                                        {event.startDate
-                                            ? event.startDate.toFormat("yyyy-MM-dd HH:mm:ss")
-                                            : "kein datum"}
-                                    </td>
-                                    <td>{event.location}</td>
-                                    <td>{event.duration}</td>
-                                    <td>
-                                        <button
-                                            className="btn btn-primary"
-                                            onClick={() => importEvent(event)}
-                                            disabled={
-                                                lockedIds === undefined ||
-                                                lockedIds.includes(event.locally_unique_id)
-                                            }
-                                        >
-                                            Anlegen
-                                        </button>
-                                    </td>
-                                </tr>
-                            ))}
-                        </tbody>
-                    </table>
-                </div>
-            </div>
-            <div className="card">
-                <div className="card-header d-flex align-items-center">
-                    <span className="flex-grow-1">Bei uns, nicht im Import</span>
-
-                    <button
-                        className="btn btn-primary"
-                        onClick={removeAllEvents}
-                        disabled={lockedIds === undefined || lockedIds.length > 0}
-                    >
-                        Alle entfernen
-                    </button>
-                </div>
-                <div className="card-body">
-                    <table className="table">
-                        <thead>
-                            <tr>
-                                <th>Zeit</th>
-                                <th>Ort</th>
-                                <th>Dauer</th>
-                                <th></th>
+                        ))}
+                    </tbody>
+                </table>
+            </Card>
+
+            <Card
+                header={
+                    <>
+                        <span className="flex-grow-1">Bei uns, nicht im Import</span>
+                        <button
+                            className="btn btn-primary"
+                            onClick={removeAllEvents}
+                            disabled={lockedIds === undefined || lockedIds.length > 0}
+                        >
+                            Alle entfernen
+                        </button>
+                    </>
+                }
+            >
+                <table className="table">
+                    <thead>
+                        <tr>
+                            <th>Zeit</th>
+                            <th>Ort</th>
+                            <th>Dauer</th>
+                            <th></th>
+                        </tr>
+                    </thead>
+                    <tbody>
+                        {not_imported_events.map((event, index) => (
+                            <tr key={index}>
+                                <td>
+                                    {event.startDate
+                                        ? event.startDate.toFormat("yyyy-MM-dd HH:mm:ss")
+                                        : "kein datum"}
+                                </td>
+                                <td>{event.location}</td>
+                                <td>{event.duration}</td>
+                                <td>
+                                    <button
+                                        className="btn btn-primary"
+                                        onClick={() => removeEvent(event)}
+                                        disabled={
+                                            lockedIds === undefined ||
+                                            lockedIds.includes(event.locally_unique_id)
+                                        }
+                                    >
+                                        Entfernen
+                                    </button>
+                                </td>
                             </tr>
-                        </thead>
-                        <tbody>
-                            {not_imported_events.map((event, index) => (
-                                <tr key={index}>
-                                    <td>
-                                        {event.startDate
-                                            ? event.startDate.toFormat("yyyy-MM-dd HH:mm:ss")
-                                            : "kein datum"}
-                                    </td>
-                                    <td>{event.location}</td>
-                                    <td>{event.duration}</td>
-                                    <td>
-                                        <button
-                                            className="btn btn-primary"
-                                            onClick={() => removeEvent(event)}
-                                            disabled={
-                                                lockedIds === undefined ||
-                                                lockedIds.includes(event.locally_unique_id)
-                                            }
-                                        >
-                                            Entfernen
-                                        </button>
-                                    </td>
-                                </tr>
-                            ))}
-                        </tbody>
-                    </table>
-                </div>
-            </div>
+                        ))}
+                    </tbody>
+                </table>
+            </Card>
         </div>
     );
 }
@@ -469,59 +470,52 @@ function RWTHOnlineImportImpl() {
             <Link href={`/${course.handle}`} className="btn btn-primary mb-2">
                 <span className="bi bi-chevron-left" /> Zum Kurs
             </Link>
-            <div className="card mb-2">
-                <div className="card-header d-flex">Import von RWTHOnline</div>
-                <div className="card-body">
-                    <a
-                        href="https://online.rwth-aachen.de/RWTHonline/pl/ui/%24ctx/wbSuche.LVSuche"
-                        target="_blank"
-                    >
-                        Suche den Kurs auf RWTHOnline
-                    </a>
-                    , und kopiere die URL hier rein:
-                    <div>
-                        <input
-                            type="text"
-                            className="w-100"
-                            placeholder={exampleImportUrl}
-                            ref={inputRef}
-                        />
-                        {isImporting ? (
-                            <div className="spinner-border mt-2" role="status" />
-                        ) : (
-                            <button
-                                className="btn btn-primary mt-2"
-                                type="button"
-                                onClick={onImport}
-                            >
-                                Zum Import hinzufügen
-                            </button>
-                        )}
-                    </div>
-                    <div>
-                        Bereits importierte Termine: {imported_events.current.length}
-                        <table className="table table-bordered">
-                            <tbody>
-                                {imported_courses.current.map((courseId, index) => (
-                                    <tr key={index}>
-                                        <td>
-                                            {courseId}
-
-                                            <button
-                                                type="button"
-                                                className="ms-2 btn btn-danger"
-                                                onClick={() => removeImportedCourse(courseId)}
-                                            >
-                                                <i className="bi bi-trash-fill" />
-                                            </button>
-                                        </td>
-                                    </tr>
-                                ))}
-                            </tbody>
-                        </table>
-                    </div>
+            <Card header="Import von RWTHOnline">
+                <a
+                    href="https://online.rwth-aachen.de/RWTHonline/pl/ui/%24ctx/wbSuche.LVSuche"
+                    target="_blank"
+                >
+                    Suche den Kurs auf RWTHOnline
+                </a>
+                , und kopiere die URL hier rein:
+                <div>
+                    <input
+                        type="text"
+                        className="w-100"
+                        placeholder={exampleImportUrl}
+                        ref={inputRef}
+                    />
+                    {isImporting ? (
+                        <div className="spinner-border mt-2" role="status" />
+                    ) : (
+                        <button className="btn btn-primary mt-2" type="button" onClick={onImport}>
+                            Zum Import hinzufügen
+                        </button>
+                    )}
                 </div>
-            </div>
+                <div>
+                    Bereits importierte Termine: {imported_events.current.length}
+                    <table className="table table-bordered">
+                        <tbody>
+                            {imported_courses.current.map((courseId, index) => (
+                                <tr key={index}>
+                                    <td>
+                                        {courseId}
+
+                                        <button
+                                            type="button"
+                                            className="ms-2 btn btn-danger"
+                                            onClick={() => removeImportedCourse(courseId)}
+                                        >
+                                            <i className="bi bi-trash-fill" />
+                                        </button>
+                                    </td>
+                                </tr>
+                            ))}
+                        </tbody>
+                    </table>
+                </div>
+            </Card>
             <TerminBody course={course} imported_events={imported_events} />
         </ReloadBoundary>
     );
diff --git a/src/pages/internal/jobs.tsx b/src/pages/internal/jobs.tsx
index 6b55ec14c8a5fb68ca62573ef705dac848c7d0b8..e4fae426d9d30fa83abd7935b6b2921fd2bfb848 100644
--- a/src/pages/internal/jobs.tsx
+++ b/src/pages/internal/jobs.tsx
@@ -3,7 +3,7 @@ import { useApi, JOB_STATES } from "@/api";
 import { ModeratorBarrier } from "@/authentication";
 import { ErrorComponent } from "@/error";
 import { StringEditor, ChooserEditor } from "@/form";
-import { ReloadBoundary, ExpandableString, datetimeToString, Spinner } from "@/miscellaneous";
+import { Card, ReloadBoundary, ExpandableString, datetimeToString, Spinner } from "@/miscellaneous";
 import { useFilteredDataView, PagingNavigation, FilterInput } from "@/object_management";
 
 function JobList({ jobsResp }: { jobsResp: GetJobsResponse }) {
@@ -113,52 +113,49 @@ function JobsImpl() {
             }),
     );
     return (
-        <div className="card">
-            <div className="card-header d-flex">Jobs</div>
-            <div className="card-body">
-                <p>Hier werden alle Jobs angezeigt</p>
-                <ReloadBoundary reloadFunc={reloadData}>
-                    <div className="d-flex align-items-center">
-                        <FilterInput
-                            component={(props) => (
-                                <ChooserEditor
-                                    langKeyPrefix="job_state"
-                                    unchosenLangKey="ui.generic.filter.any"
-                                    possibleValues={JOB_STATES}
-                                    {...props}
-                                />
-                            )}
-                            label="State"
-                            filterId="state"
-                            setFilter={setFilter}
-                            filters={filters}
-                        />
-                        <FilterInput
-                            component={(props) => (
-                                <StringEditor maxLength={100} allowUndefined={true} {...props} />
-                            )}
-                            label="Type"
-                            filterId="type"
-                            setFilter={setFilter}
-                            filters={filters}
-                        />
-                    </div>
-                    <div className="d-flex">
-                        <PagingNavigation
-                            setFilter={setFilter}
-                            filters={filters}
-                            pageCount={data?.page_count}
-                        />
-                        <Spinner visible={isLoading} />
-                    </div>
-                    {error ? (
-                        <ErrorComponent error={error} objectName="Jobs" showButtons={false} />
-                    ) : (
-                        data && <JobList jobsResp={data} />
-                    )}
-                </ReloadBoundary>
-            </div>
-        </div>
+        <Card header="Jobs">
+            <p>Hier werden alle Jobs angezeigt</p>
+            <ReloadBoundary reloadFunc={reloadData}>
+                <div className="d-flex align-items-center">
+                    <FilterInput
+                        component={(props) => (
+                            <ChooserEditor
+                                langKeyPrefix="job_state"
+                                unchosenLangKey="ui.generic.filter.any"
+                                possibleValues={JOB_STATES}
+                                {...props}
+                            />
+                        )}
+                        label="State"
+                        filterId="state"
+                        setFilter={setFilter}
+                        filters={filters}
+                    />
+                    <FilterInput
+                        component={(props) => (
+                            <StringEditor maxLength={100} allowUndefined={true} {...props} />
+                        )}
+                        label="Type"
+                        filterId="type"
+                        setFilter={setFilter}
+                        filters={filters}
+                    />
+                </div>
+                <div className="d-flex">
+                    <PagingNavigation
+                        setFilter={setFilter}
+                        filters={filters}
+                        pageCount={data?.page_count}
+                    />
+                    <Spinner visible={isLoading} />
+                </div>
+                {error ? (
+                    <ErrorComponent error={error} objectName="Jobs" showButtons={false} />
+                ) : (
+                    data && <JobList jobsResp={data} />
+                )}
+            </ReloadBoundary>
+        </Card>
     );
 }
 
diff --git a/src/pages/internal/sorter_files.tsx b/src/pages/internal/sorter_files.tsx
index 4628b4655431b36b3f19e4a2deaf611013e97d95..657a32f880c96c3827a91350bfad94b25a84b350 100644
--- a/src/pages/internal/sorter_files.tsx
+++ b/src/pages/internal/sorter_files.tsx
@@ -7,7 +7,13 @@ import { ModeratorBarrier } from "@/authentication";
 import { showError, ErrorComponent } from "@/error";
 import { BooleanEditor } from "@/form";
 import { JobStatusCard } from "@/job";
-import { ReloadBoundary, ExpandableString, datetimeToString, showInfoToast } from "@/miscellaneous";
+import {
+    Card,
+    ReloadBoundary,
+    ExpandableString,
+    datetimeToString,
+    showInfoToast,
+} from "@/miscellaneous";
 import {
     useFilteredDataView,
     PagingNavigation,
@@ -141,59 +147,52 @@ function SorterFilesImpl() {
     };
 
     return (
-        <div className="card">
-            <div className="card-header d-flex">Sorter Files</div>
-            <div className="card-body">
-                <div className="d-flex align-items-center mb-4">
-                    <button
-                        onClick={runSourceFileSorter}
-                        disabled={doingSorterRequest}
-                        className="btn btn-primary"
-                    >
-                        Run Sorter
-                    </button>
-                    {doingSorterRequest && <div className="spinner-border ms-3 mt-1" />}
-                    {sorterJobId && (
-                        <span className="m-2">
-                            <JobStatusCard jobId={sorterJobId} />
-                        </span>
-                    )}
-                </div>
-
-                <p>
-                    Hier werden alle Dateien angezeigt welche einsortiert wurden, bzw. wo es einen
-                    Fehler beim Sortieren gab.
-                </p>
-                <ReloadBoundary reloadFunc={reloadData}>
-                    <div className="d-flex align-items-center">
-                        <FilterInput
-                            component={(props) => <BooleanEditor {...props} />}
-                            label="Include Sorted"
-                            filterId="include_sorted"
-                            setFilter={setFilter}
-                            filters={filters}
-                        />
-                    </div>
-                    <div className="d-flex">
-                        <PagingNavigation
-                            setFilter={setFilter}
-                            filters={filters}
-                            pageCount={data?.page_count}
-                        />
-                        {isLoading && <div className="spinner-border ms-3 mt-1" />}
-                    </div>
-                    {error ? (
-                        <ErrorComponent
-                            error={error}
-                            objectName="Sorter Files"
-                            showButtons={false}
-                        />
-                    ) : (
-                        data && <SorterFileList sorterFilesResp={data} />
-                    )}
-                </ReloadBoundary>
+        <Card header="Sorter Files">
+            <div className="d-flex align-items-center mb-4">
+                <button
+                    onClick={runSourceFileSorter}
+                    disabled={doingSorterRequest}
+                    className="btn btn-primary"
+                >
+                    Run Sorter
+                </button>
+                {doingSorterRequest && <div className="spinner-border ms-3 mt-1" />}
+                {sorterJobId && (
+                    <span className="m-2">
+                        <JobStatusCard jobId={sorterJobId} />
+                    </span>
+                )}
             </div>
-        </div>
+
+            <p>
+                Hier werden alle Dateien angezeigt welche einsortiert wurden, bzw. wo es einen
+                Fehler beim Sortieren gab.
+            </p>
+            <ReloadBoundary reloadFunc={reloadData}>
+                <div className="d-flex align-items-center">
+                    <FilterInput
+                        component={(props) => <BooleanEditor {...props} />}
+                        label="Include Sorted"
+                        filterId="include_sorted"
+                        setFilter={setFilter}
+                        filters={filters}
+                    />
+                </div>
+                <div className="d-flex">
+                    <PagingNavigation
+                        setFilter={setFilter}
+                        filters={filters}
+                        pageCount={data?.page_count}
+                    />
+                    {isLoading && <div className="spinner-border ms-3 mt-1" />}
+                </div>
+                {error ? (
+                    <ErrorComponent error={error} objectName="Sorter Files" showButtons={false} />
+                ) : (
+                    data && <SorterFileList sorterFilesResp={data} />
+                )}
+            </ReloadBoundary>
+        </Card>
     );
 }
 
diff --git a/src/pages/internal/timetable.tsx b/src/pages/internal/timetable.tsx
index 8d0611eb40c74e114112d893ea3fb32ef3ee46ff..f1b96ed769b12643470068a12775a91b8231e121 100644
--- a/src/pages/internal/timetable.tsx
+++ b/src/pages/internal/timetable.tsx
@@ -2,6 +2,7 @@ import Link from "next/link";
 import { DateTime } from "luxon";
 
 import { ModeratorBarrier } from "@/authentication";
+import { Card } from "@/miscellaneous";
 
 interface Days {
     index: number;
@@ -168,14 +169,11 @@ function TimetableTable() {
 
 function TimetableImpl() {
     return (
-        <div className="card">
-            <div className="card-header d-flex">Timetable</div>
-            <div className="card-body">
-                This page is a work in progress, the data here is not real.
-                <hr />
-                <TimetableTable />
-            </div>
-        </div>
+        <Card header="Timetable">
+            This page is a work in progress, the data here is not real.
+            <hr />
+            <TimetableTable />
+        </Card>
         /* TODO: pagination
 		<div className="hidden-print">
 			<div style="margin-top: 10px; padding: 15px;" className="col-xs-12">
diff --git a/src/styles/globals.scss b/src/styles/globals.scss
index 0eb68c04206aabba1cd4fb652f659037bfd8cf6a..547d93c584a6a0fd6bfae50268f27e5fe07827b8 100644
--- a/src/styles/globals.scss
+++ b/src/styles/globals.scss
@@ -99,6 +99,14 @@ html:has(> body.modal-open) {
 	background-color: inherit; // this fixes tables messing up background colors of parent elements
 }
 
+.card-header:not(:has(+ .card-body)) {
+    border-bottom: 0;
+}
+
+.card-header {
+    --bs-card-cap-padding-y: 0.6em;
+}
+
 .icon-rwth {
 	display: inline-block;
 	vertical-align: middle;
diff --git a/src/videoag/course/CourseListing.tsx b/src/videoag/course/CourseListing.tsx
index aa3adbe529d493a9883f19a41447f507ced160d5..bfffb0b4479c50ef2603382c91ca7ec896c060bc 100644
--- a/src/videoag/course/CourseListing.tsx
+++ b/src/videoag/course/CourseListing.tsx
@@ -3,7 +3,7 @@ import Link from "next/link";
 import type { GetCourseResponse } from "@/api/types";
 import { useAuthStatus, AuthenticationMethodIcons } from "@/authentication";
 import { useLanguage } from "@/localization";
-import { Title, UpdateOverlay } from "@/miscellaneous";
+import { Card, Title, UpdateOverlay } from "@/miscellaneous";
 import {
     useEditMode,
     OMCreate,
@@ -23,9 +23,10 @@ function ListingHeader({ course }: { course: GetCourseResponse }) {
     const { language } = useLanguage();
 
     return (
-        <div className={`card mb-3 ${course.visible === false ? "bg-danger-subtle" : ""}`}>
-            <div className="card-body">
-                <h5 className="card-title d-flex">
+        <Card
+            objectVisible={course.visible}
+            header={
+                <>
                     <span className="panel-title flex-fill">
                         <EmbeddedOMFieldComponent
                             object_type="course"
@@ -58,92 +59,82 @@ function ListingHeader({ course }: { course: GetCourseResponse }) {
                             authentication_methods={course.default_authentication_methods}
                         />
                     </div>
-                </h5>
-                <div className="row">
-                    <div className="col-12">
-                        <Link
-                            href={`/courses#course-${course.id}`}
-                            className="btn btn-primary mb-1"
-                        >
-                            <span className="bi bi-chevron-left" />{" "}
-                            {language.get("ui.course.back_to_list")}
-                        </Link>
-                        <table className="table table-sm">
-                            <tbody>
-                                <tr>
-                                    <td>{language.get("object.course.semester")}:</td>
-                                    <td>
-                                        <EmbeddedOMFieldComponent
-                                            object_type="course"
-                                            object_id={course.id!}
-                                            field_id="semester"
-                                            field_type="semester_string"
-                                            initialValue={course.semester}
-                                        />
-                                    </td>
-                                </tr>
-                                <tr>
-                                    <td>{language.get("object.course.organizer")}:</td>
-                                    <td>
-                                        <EmbeddedOMFieldComponent
-                                            object_type="course"
-                                            object_id={course.id!}
-                                            field_id="organizer"
-                                            field_type="string"
-                                            initialValue={course.organizer}
-                                            allowMarkdown={true}
-                                        />
-                                    </td>
-                                </tr>
-                                <tr>
-                                    <td className="w-25">
-                                        {language.get("object.course.description")}:
-                                    </td>
-                                    <td>
-                                        <EmbeddedOMFieldComponent
-                                            object_type="course"
-                                            object_id={course.id!}
-                                            field_id="description"
-                                            field_type="string"
-                                            initialValue={course.description}
-                                            allowMarkdown={true}
-                                        />
-                                    </td>
-                                </tr>
-                                {hasUserInfo && (
-                                    <>
-                                        <tr>
-                                            <td className="w-25">
-                                                {language.get("object.course.internal_comment")}:
-                                            </td>
-                                            <td>
-                                                <EmbeddedOMFieldComponent
-                                                    object_type="course"
-                                                    object_id={course.id!}
-                                                    field_id="internal_comment"
-                                                    field_type="string"
-                                                    initialValue={course.internal_comment}
-                                                    allowMarkdown={true}
-                                                />
-                                            </td>
-                                        </tr>
-                                    </>
-                                )}
-                            </tbody>
-                        </table>
-                        <DownloadAllModal course={course} />
-                    </div>
+                </>
+            }
+        >
+            <div className="row">
+                <div className="col-12">
+                    <Link href={`/courses#course-${course.id}`} className="btn btn-primary mb-1">
+                        <span className="bi bi-chevron-left" />{" "}
+                        {language.get("ui.course.back_to_list")}
+                    </Link>
+                    <table className="table table-sm">
+                        <tbody>
+                            <tr>
+                                <td>{language.get("object.course.semester")}:</td>
+                                <td>
+                                    <EmbeddedOMFieldComponent
+                                        object_type="course"
+                                        object_id={course.id!}
+                                        field_id="semester"
+                                        field_type="semester_string"
+                                        initialValue={course.semester}
+                                    />
+                                </td>
+                            </tr>
+                            <tr>
+                                <td>{language.get("object.course.organizer")}:</td>
+                                <td>
+                                    <EmbeddedOMFieldComponent
+                                        object_type="course"
+                                        object_id={course.id!}
+                                        field_id="organizer"
+                                        field_type="string"
+                                        initialValue={course.organizer}
+                                        allowMarkdown={true}
+                                    />
+                                </td>
+                            </tr>
+                            <tr>
+                                <td className="w-25">
+                                    {language.get("object.course.description")}:
+                                </td>
+                                <td>
+                                    <EmbeddedOMFieldComponent
+                                        object_type="course"
+                                        object_id={course.id!}
+                                        field_id="description"
+                                        field_type="string"
+                                        initialValue={course.description}
+                                        allowMarkdown={true}
+                                    />
+                                </td>
+                            </tr>
+                            {hasUserInfo && (
+                                <>
+                                    <tr>
+                                        <td className="w-25">
+                                            {language.get("object.course.internal_comment")}:
+                                        </td>
+                                        <td>
+                                            <EmbeddedOMFieldComponent
+                                                object_type="course"
+                                                object_id={course.id!}
+                                                field_id="internal_comment"
+                                                field_type="string"
+                                                initialValue={course.internal_comment}
+                                                allowMarkdown={true}
+                                            />
+                                        </td>
+                                    </tr>
+                                </>
+                            )}
+                        </tbody>
+                    </table>
+                    <DownloadAllModal course={course} />
                 </div>
-                {course.visible === false && (
-                    <>
-                        <hr />
-                        <small className="text-muted">
-                            {language.get("ui.generic.object_invisible")}
-                        </small>
-                    </>
-                )}
             </div>
-        </div>
+        </Card>
     );
 }
 
@@ -151,9 +142,9 @@ function ListingBody({ course }: { course: GetCourseResponse }) {
     const { editMode } = useEditMode();
 
     return (
-        <div className="card mb-3">
-            <div className="card-body">
-                <h5 className="card-title d-flex align-items-center">
+        <Card
+            header={
+                <>
                     Videos
                     <div className="flex-fill" />
                     {editMode && (
@@ -177,19 +168,20 @@ function ListingBody({ course }: { course: GetCourseResponse }) {
                             </Link>
                         </>
                     )}
-                </h5>
-                <ul className="list-group lectureslist">
-                    {course.lectures!.map((lecture) => (
-                        <LectureListItem
-                            key={lecture.id}
-                            lecture={lecture}
-                            courseHandle={course.handle}
-                            show_chapters={course.show_chapters_on_course}
-                        />
-                    ))}
-                </ul>
-            </div>
-        </div>
+                </>
+            }
+        >
+            <ul className="list-group lectureslist">
+                {course.lectures!.map((lecture) => (
+                    <LectureListItem
+                        key={lecture.id}
+                        lecture={lecture}
+                        courseHandle={course.handle}
+                        show_chapters={course.show_chapters_on_course}
+                    />
+                ))}
+            </ul>
+        </Card>
     );
 }
 
diff --git a/src/videoag/course/Medium.tsx b/src/videoag/course/Medium.tsx
index 896e13a656beed97b68f9f488878b0ed1fcda1b8..83022c7384c002b2cdb038d2cfa4eb0c738b9119 100644
--- a/src/videoag/course/Medium.tsx
+++ b/src/videoag/course/Medium.tsx
@@ -7,8 +7,9 @@ import { showError, ErrorComponent } from "@/error";
 import { JobStatusCard } from "@/job";
 import { useLanguage } from "@/localization";
 import {
+    Card,
     Spinner,
-    ReloadBoundary,
+    NestedReloadBoundary,
     useMutexCall,
     showInfoToast,
     TooltipButton,
@@ -390,69 +391,69 @@ function MediumFileListItem({
     }
 
     return (
-        <div className={"card mb-3 " + (file.to_be_replaced ? "bg-warning-subtle" : "")}>
-            <div id={`medium_file_${file.id}`} className="card-body">
-                <h5>Medium File {file.id}</h5>
-                <table className="table">
-                    <thead>
-                        <tr>
-                            <th scope="col" style={{ width: "30%" }} />
-                            <th scope="col" style={{ width: "70%" }} />
-                        </tr>
-                    </thead>
-                    <tbody>
-                        <tr>
-                            <td>File Path</td>
-                            <td>{file.file_path}</td>
-                        </tr>
-                        <tr>
-                            <td>Input Data SHA256</td>
-                            <td>{file.input_data_sha256}</td>
-                        </tr>
-                        <tr>
-                            <td>Process SHA256</td>
-                            <td>{file.process_sha256}</td>
-                        </tr>
-                        <tr>
-                            <td>Process Target ID</td>
-                            <td>{file.process_target_id}</td>
-                        </tr>
-                        <tr>
-                            <td>Producer Job</td>
-                            <td>
-                                {file.producer_job_id &&
-                                    (showJobStatus ? (
-                                        <JobStatusCard jobId={file.producer_job_id} />
-                                    ) : (
-                                        <>
-                                            <span>{file.producer_job_id}</span>
-                                            <button
-                                                className="btn btn-secondary ms-2"
-                                                onClick={(e) => setShowJobStatus(true)}
-                                            >
-                                                Show Status
-                                            </button>
-                                        </>
-                                    ))}
-                            </td>
-                        </tr>
-                        <tr>
-                            <td>
-                                To Be Replaced?
-                                <TooltipButton>
-                                    Indicates whether this medium file is outdated due to various
-                                    reasons. Usually because the process changed or the input
-                                    dependencies changed.
-                                </TooltipButton>
-                            </td>
-                            <td>{file.to_be_replaced ? "Yes" : "No"}</td>
-                        </tr>
-                    </tbody>
-                </table>
-                {metadataComp}
-                {publishMediumComp}
-            </div>
-        </div>
+        <Card
+            header={`Medium File ${file.id}`}
+            className={file.to_be_replaced ? "bg-warning-subtle" : ""}
+        >
+            <table className="table">
+                <thead>
+                    <tr>
+                        <th scope="col" style={{ width: "30%" }} />
+                        <th scope="col" style={{ width: "70%" }} />
+                    </tr>
+                </thead>
+                <tbody>
+                    <tr>
+                        <td>File Path</td>
+                        <td>{file.file_path}</td>
+                    </tr>
+                    <tr>
+                        <td>Input Data SHA256</td>
+                        <td>{file.input_data_sha256}</td>
+                    </tr>
+                    <tr>
+                        <td>Process SHA256</td>
+                        <td>{file.process_sha256}</td>
+                    </tr>
+                    <tr>
+                        <td>Process Target ID</td>
+                        <td>{file.process_target_id}</td>
+                    </tr>
+                    <tr>
+                        <td>Producer Job</td>
+                        <td>
+                            {file.producer_job_id &&
+                                (showJobStatus ? (
+                                    <JobStatusCard jobId={file.producer_job_id} />
+                                ) : (
+                                    <>
+                                        <span>{file.producer_job_id}</span>
+                                        <button
+                                            className="btn btn-secondary ms-2"
+                                            onClick={(e) => setShowJobStatus(true)}
+                                        >
+                                            Show Status
+                                        </button>
+                                    </>
+                                ))}
+                        </td>
+                    </tr>
+                    <tr>
+                        <td>
+                            To Be Replaced?
+                            <TooltipButton>
+                                Indicates whether this medium file is outdated due to various
+                                reasons. Usually because the process changed or the input
+                                dependencies changed.
+                            </TooltipButton>
+                        </td>
+                        <td>{file.to_be_replaced ? "Yes" : "No"}</td>
+                    </tr>
+                </tbody>
+            </table>
+            {metadataComp}
+            {publishMediumComp}
+        </Card>
     );
 }
 
@@ -466,57 +467,53 @@ function SorterFileListItem({
     const file = context.sorter_files[sorterFileId.toString()]!;
 
     return (
-        <div className="card mb-3">
-            <div className="card-body">
-                <h5>Sorter File {file.id}</h5>
-                <table className="table">
-                    <thead>
-                        <tr>
-                            <th scope="col" style={{ width: "30%" }} />
-                            <th scope="col" style={{ width: "70%" }} />
-                        </tr>
-                    </thead>
-                    <tbody>
-                        <tr>
-                            <td>File Path</td>
-                            <td>{file.file_path}</td>
-                        </tr>
-                        <tr>
-                            <td>File Modification Time</td>
-                            <td>{datetimeToString(file.file_modification_time)}</td>
-                        </tr>
-                        <tr>
-                            <td>SHA256</td>
-                            <td>{file.sha256}</td>
-                        </tr>
-                        <tr>
-                            <td>Tag</td>
-                            <td>{file.tag}</td>
-                        </tr>
-                        <tr>
-                            <td>
-                                Designated Medium File ID
-                                <TooltipButton>
-                                    Shows which medium file was created for this source file. That
-                                    medium file and this source file reference the same file (have
-                                    the same file path).
-                                    <br />
-                                    Note that this medium file can change if the process changes,
-                                    etc.
-                                </TooltipButton>
-                            </td>
-                            <td>
-                                {file.designated_medium_file_id && (
-                                    <a href={`#medium_file_${file.designated_medium_file_id}`}>
-                                        {file.designated_medium_file_id}
-                                    </a>
-                                )}
-                            </td>
-                        </tr>
-                    </tbody>
-                </table>
-            </div>
-        </div>
+        <Card header={`Sorter File ${file.id}`}>
+            <table className="table">
+                <thead>
+                    <tr>
+                        <th scope="col" style={{ width: "30%" }} />
+                        <th scope="col" style={{ width: "70%" }} />
+                    </tr>
+                </thead>
+                <tbody>
+                    <tr>
+                        <td>File Path</td>
+                        <td>{file.file_path}</td>
+                    </tr>
+                    <tr>
+                        <td>File Modification Time</td>
+                        <td>{datetimeToString(file.file_modification_time)}</td>
+                    </tr>
+                    <tr>
+                        <td>SHA256</td>
+                        <td>{file.sha256}</td>
+                    </tr>
+                    <tr>
+                        <td>Tag</td>
+                        <td>{file.tag}</td>
+                    </tr>
+                    <tr>
+                        <td>
+                            Designated Medium File ID
+                            <TooltipButton>
+                                Shows which medium file was created for this source file. That
+                                medium file and this source file reference the same file (have the
+                                same file path).
+                                <br />
+                                Note that this medium file can change if the process changes, etc.
+                            </TooltipButton>
+                        </td>
+                        <td>
+                            {file.designated_medium_file_id && (
+                                <a href={`#medium_file_${file.designated_medium_file_id}`}>
+                                    {file.designated_medium_file_id}
+                                </a>
+                            )}
+                        </td>
+                    </tr>
+                </tbody>
+            </table>
+        </Card>
     );
 }
 
@@ -580,7 +577,7 @@ export function MediaProcessOverview({ lectureId }: { lectureId: int }) {
     }
 
     return (
-        <NestedReloadBoundary reloadFunc={reloadData} >
+        <NestedReloadBoundary reloadFunc={reloadData}>
             <ul className="list-unstyled">
                 <table className="table">
                     <thead>
@@ -612,10 +609,11 @@ export function MediaProcessOverview({ lectureId }: { lectureId: int }) {
                             <td>
                                 Is automatic Media Process Scheduler enabled?{" "}
                                 <TooltipButton>
-                                    This shows whether the media process scheduler will automatically
-                                    run when any changes occur that might require processing media
-                                    files. This can be changed in the lecture config or, if the lecture
-                                    has specified &apos;Inherit&apos;, in the course config.
+                                    This shows whether the media process scheduler will
+                                    automatically run when any changes occur that might require
+                                    processing media files. This can be changed in the lecture
+                                    config or, if the lecture has specified &apos;Inherit&apos;, in
+                                    the course config.
                                 </TooltipButton>
                             </td>
                             <td>
@@ -635,15 +633,15 @@ export function MediaProcessOverview({ lectureId }: { lectureId: int }) {
                         Run Media Process Scheduler Manually
                     </button>
                     <TooltipButton>
-                        If the automatic media process scheduler is disabled, you can run it manually
-                        here (You will need to run it multiple times until the process is finished;
-                        after every finished job).
+                        If the automatic media process scheduler is disabled, you can run it
+                        manually here (You will need to run it multiple times until the process is
+                        finished; after every finished job).
                         <br />
                         <br />
                         If a job failed the automatic process scheduler will also not run again
-                        automatically (to prevent infinite loops) and you can run it manually. It will
-                        automatically detect the failed job and schedule a new one which might or might
-                        not fail again.
+                        automatically (to prevent infinite loops) and you can run it manually. It
+                        will automatically detect the failed job and schedule a new one which might
+                        or might not fail again.
                         <br />
                         <br />
                         In all other cases you do not need to run it manually.
diff --git a/src/videoag/course/Player.tsx b/src/videoag/course/Player.tsx
index c2306106e85560dc3516cdebc035ac7acd0473fa..0fb1c9279c3f441d50fd60799c4ac3e92f62056c 100644
--- a/src/videoag/course/Player.tsx
+++ b/src/videoag/course/Player.tsx
@@ -14,7 +14,7 @@ import type { course, chapter, publish_medium, lecture } from "@/api/types";
 import { useApi } from "@/api";
 import { ViewPermissionAuthorization } from "@/authentication";
 import { useLanguage } from "@/localization";
-import { Title, UpdateOverlay, showInfoToast, parseApiDatetime } from "@/miscellaneous";
+import { Card, Title, UpdateOverlay, showInfoToast, parseApiDatetime } from "@/miscellaneous";
 import {
     useEditMode,
     OMDelete,
@@ -24,7 +24,7 @@ import {
 } from "@/object_management";
 import { basePath } from "#basepath";
 
-import { LectureCard, urlForLecture, getLectureThumbnailUrlNoPlaceholder } from "./Lecture";
+import { LectureCard, getLectureThumbnailUrlNoPlaceholder } from "./Lecture";
 import {
     PublishMediumDownloadButton,
     getSortedPlayerPublishMedia,
@@ -237,8 +237,34 @@ function VideoPlayer({
     );
 }
 
-function LectureSuggestions({ course, lecture }: { course: course; lecture: lecture }) {
+function LectureSuggestionCard({
+    course,
+    lecture,
+    isPrevious,
+}: {
+    course: course;
+    lecture: lecture;
+    isPrevious: boolean;
+}) {
     const { language } = useLanguage();
+    return (
+        <Card
+            objectVisible={lecture.visible}
+            headerNoStyling={true}
+            header={
+                <span className="d-flex justify-content-center">
+                    {isPrevious && <i className="bi bi-arrow-left-circle me-2" />}
+                    {language.get(`ui.video_player.${isPrevious ? "previous" : "next"}_lecture`)}
+                    {!isPrevious && <i className="bi bi-arrow-right-circle ms-2" />}
+                </span>
+            }
+        >
+            <LectureCard course={course} lecture={lecture} size="small" />
+        </Card>
+    );
+}
+
+function LectureSuggestions({ course, lecture }: { course: course; lecture: lecture }) {
     const prevLecture = course.lectures?.findLast(
         (l) =>
             parseApiDatetime(l.time) < parseApiDatetime(lecture.time) &&
@@ -253,44 +279,11 @@ function LectureSuggestions({ course, lecture }: { course: course; lecture: lect
     return (
         <div className="d-flex w-100 flex-column flex-sm-row">
             {prevLecture && (
-                <div className={`card ${prevLecture.visible === false ? "bg-danger-subtle" : ""}`}>
-                    <Link
-                        className="card-header text-center bg-light-subtle"
-                        href={urlForLecture(course.handle, prevLecture.id)}
-                    >
-                        <i className="bi bi-arrow-left-circle me-2" />
-                        {language.get("ui.video_player.previous_lecture")}
-                    </Link>
-                    <div className="card-body">
-                        <LectureCard course={course} lecture={prevLecture} size="small" />
-                    </div>
-                    {prevLecture.visible === false && (
-                        <div className="card-footer text-center text-muted">
-                            {language.get("ui.generic.object_invisible")}
-                        </div>
-                    )}
-                </div>
+                <LectureSuggestionCard course={course} lecture={prevLecture} isPrevious={true} />
             )}
-
             <div className="flex-fill p-2" />
             {nextLecture && (
-                <div className={`card ${nextLecture.visible === false ? "bg-danger-subtle" : ""}`}>
-                    <Link
-                        className="card-header text-center bg-light-subtle"
-                        href={urlForLecture(course.handle, nextLecture.id)}
-                    >
-                        {language.get("ui.video_player.next_lecture")}
-                        <i className="bi bi-arrow-right-circle ms-2" />
-                    </Link>
-                    <div className="card-body">
-                        <LectureCard course={course} lecture={nextLecture} size="small" />
-                    </div>
-                    {nextLecture.visible === false && (
-                        <div className="card-footer text-center text-muted">
-                            {language.get("ui.generic.object_invisible")}
-                        </div>
-                    )}
-                </div>
+                <LectureSuggestionCard course={course} lecture={nextLecture} isPrevious={false} />
             )}
         </div>
     );
@@ -473,34 +466,36 @@ export default function Player({
             </Head>
             <Title title={`${course.short_name} - ${lecture.title}`} />
             <UpdateOverlay show={disabled} />
-            <div className={`card ${lecture.visible === false ? "bg-danger-subtle" : ""}`}>
-                <div className="card-header d-flex">
-                    <div className="flex-fill align-self-center">
-                        <strong>
-                            <Link href={`/${course.handle}#lecture-${lecture.id}`}>
-                                {course.full_name}
-                            </Link>
-                        </strong>
-                        :{" "}
-                        <EmbeddedOMFieldComponent
-                            object_type="lecture"
-                            object_id={lecture.id!}
-                            field_id="title"
-                            field_type="string"
-                            initialValue={lecture.title}
-                        />{" "}
-                        (
-                        <EmbeddedOMFieldComponent
-                            object_type="lecture"
-                            object_id={lecture.id!}
-                            field_id="time"
-                            field_type="datetime"
-                            initialValue={lecture.time}
-                        />
-                        )
-                    </div>
+            <Card
+                objectVisible={lecture.visible}
+                header={
+                    <>
+                        <span>
+                            <strong>
+                                <Link href={`/${course.handle}#lecture-${lecture.id}`}>
+                                    {course.full_name}
+                                </Link>
+                            </strong>
+                            :{" "}
+                            <EmbeddedOMFieldComponent
+                                object_type="lecture"
+                                object_id={lecture.id!}
+                                field_id="title"
+                                field_type="string"
+                                initialValue={lecture.title}
+                            />{" "}
+                            (
+                            <EmbeddedOMFieldComponent
+                                object_type="lecture"
+                                object_id={lecture.id!}
+                                field_id="time"
+                                field_type="datetime"
+                                initialValue={lecture.time}
+                            />
+                            )
+                        </span>
+                        <span className="flex-fill" />
 
-                    <div>
                         {editMode && (
                             <>
                                 <EmbeddedOMFieldComponent
@@ -516,52 +511,42 @@ export default function Player({
                                 <OMDelete object_type="lecture" object_id={lecture.id} />
                             </>
                         )}
+                    </>
+                }
+            >
+                <div className="row p-0 mt-1">
+                    <div className="col-12 pb-4">
+                        <Link
+                            href={`/${course.handle}#lecture-${lecture.id}`}
+                            className="btn btn-primary"
+                        >
+                            <span className="bi bi-chevron-left" />{" "}
+                            {language.get("ui.video_player.back_to_course")}
+                        </Link>
                     </div>
                 </div>
-                <div className="card-body">
-                    <div className="row p-0 mt-1">
-                        <div className="col-12 pb-4">
-                            <Link
-                                href={`/${course.handle}#lecture-${lecture.id}`}
-                                className="btn btn-primary"
-                            >
-                                <span className="bi bi-chevron-left" />{" "}
-                                {language.get("ui.video_player.back_to_course")}
-                            </Link>
-                        </div>
+                <div className="row mb-2">{pageContent}</div>
+
+                {lecture.description.length > 0 && (
+                    <div className="mt-2">
+                        <h5>Beschreibung:</h5>
+
+                        {
+                            <EmbeddedOMFieldComponent
+                                object_type="lecture"
+                                object_id={lecture.id}
+                                field_id="description"
+                                field_type="string"
+                                initialValue={lecture.description}
+                                allowMarkdown={true}
+                            />
+                        }
                     </div>
-                    <div className="row mb-2">{pageContent}</div>
-
-                    {lecture.description.length > 0 && (
-                        <div className="mt-2">
-                            <h5>Beschreibung:</h5>
+                )}
+                <Chapters chapters={lecture.chapters} seekTo={seekTo} />
 
-                            {
-                                <EmbeddedOMFieldComponent
-                                    object_type="lecture"
-                                    object_id={lecture.id}
-                                    field_id="description"
-                                    field_type="string"
-                                    initialValue={lecture.description}
-                                    allowMarkdown={true}
-                                />
-                            }
-                        </div>
-                    )}
-                    <Chapters chapters={lecture.chapters} seekTo={seekTo} />
-
-                    <LectureSuggestions course={course} lecture={lecture} />
-
-                    {lecture.visible === false && (
-                        <>
-                            <hr />
-                            <small className="text-muted">
-                                {language.get("ui.generic.object_invisible")}
-                            </small>
-                        </>
-                    )}
-                </div>
-            </div>
+                <LectureSuggestions course={course} lecture={lecture} />
+            </Card>
         </>
     );
 }
diff --git a/src/videoag/job/Job.tsx b/src/videoag/job/Job.tsx
index 20de7a6711ca218ada56d62982230f14ba0c424c..7a98ca9ae0a2ff54224dc02a8df84f743e0a4ba6 100644
--- a/src/videoag/job/Job.tsx
+++ b/src/videoag/job/Job.tsx
@@ -2,7 +2,13 @@ import { useState, useEffect, useRef } from "react";
 
 import type { int, job, job_state } from "@/api/types";
 import { useApi } from "@/api";
-import { datetimeToString, Spinner, useDebouncedCall, useReloadBoundary } from "@/miscellaneous";
+import {
+    Card,
+    datetimeToString,
+    Spinner,
+    useDebouncedCall,
+    useReloadBoundary,
+} from "@/miscellaneous";
 
 export function JobStatusCard({ jobId }: { jobId: int }) {
     const api = useApi();
@@ -73,42 +79,41 @@ export function JobStatusCard({ jobId }: { jobId: int }) {
     }, []);
 
     return (
-        <div className="card">
-            <span className="card-header d-flex align-items-center">
-                Job {jobId}
-                <Spinner visible={isLoading} />
-            </span>
-            <div className="card-body">
-                {job !== undefined && (
-                    <table className="table">
-                        <tbody>
-                            <tr>
-                                <td>Type</td>
-                                <td>{job.type}</td>
-                            </tr>
-                            <tr>
-                                <td>State</td>
-                                <td>{job.state}</td>
-                            </tr>
-                            <tr>
-                                <td>Creation Time</td>
-                                <td>{datetimeToString(job.creation_time)}</td>
-                            </tr>
-                            <tr>
-                                <td>Run Start Time</td>
-                                <td>
-                                    {job.run_start_time && datetimeToString(job.run_start_time)}
-                                </td>
-                            </tr>
-                            <tr>
-                                <td>Run End Time</td>
-                                <td>{job.run_end_time && datetimeToString(job.run_end_time)}</td>
-                            </tr>
-                        </tbody>
-                    </table>
-                )}
-                {errorMsg && <div className="text-warning">{errorMsg}</div>}
-            </div>
-        </div>
+        <Card
+            header={
+                <>
+                    Job {jobId}
+                    <Spinner visible={isLoading} />
+                </>
+            }
+        >
+            {job !== undefined && (
+                <table className="table">
+                    <tbody>
+                        <tr>
+                            <td>Type</td>
+                            <td>{job.type}</td>
+                        </tr>
+                        <tr>
+                            <td>State</td>
+                            <td>{job.state}</td>
+                        </tr>
+                        <tr>
+                            <td>Creation Time</td>
+                            <td>{datetimeToString(job.creation_time)}</td>
+                        </tr>
+                        <tr>
+                            <td>Run Start Time</td>
+                            <td>{job.run_start_time && datetimeToString(job.run_start_time)}</td>
+                        </tr>
+                        <tr>
+                            <td>Run End Time</td>
+                            <td>{job.run_end_time && datetimeToString(job.run_end_time)}</td>
+                        </tr>
+                    </tbody>
+                </table>
+            )}
+            {errorMsg && <div className="text-warning">{errorMsg}</div>}
+        </Card>
     );
 }
diff --git a/src/videoag/miscellaneous/Card.tsx b/src/videoag/miscellaneous/Card.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..966ed375c03a2a3e568e693bc4a5345b8f37a7a5
--- /dev/null
+++ b/src/videoag/miscellaneous/Card.tsx
@@ -0,0 +1,83 @@
+import React from "react";
+import { useState } from "react";
+
+import { useLanguage } from "@/localization";
+
+export function Card({
+    header,
+    headerNoStyling,
+    className,
+    bodyVisible,
+    objectVisible,
+    children,
+}: {
+    header: any;
+    headerNoStyling?: boolean;
+    className?: string;
+    bodyVisible?: boolean;
+    objectVisible?: boolean;
+    children: React.ReactNode;
+}) {
+    const { language } = useLanguage();
+    return (
+        <div
+            className={
+                "card mt-3 mb-3 " +
+                ((objectVisible ?? true) ? "" : "bg-danger-subtle ") +
+                (className ?? "")
+            }
+        >
+            {(headerNoStyling ?? false) ? (
+                <span className="card-header">{header}</span>
+            ) : (
+                <h5 className="card-header d-flex align-items-center">{header}</h5>
+            )}
+            {(bodyVisible ?? true) && <div className="card-body">{children}</div>}
+            {!(objectVisible ?? true) && (
+                <div className="card-footer text-center text-muted">
+                    {language.get("ui.generic.object_invisible")}
+                </div>
+            )}
+        </div>
+    );
+}
+
+export function CollapsableCard({
+    header,
+    headerNoStyling,
+    defaultVisible,
+    className,
+    objectVisible,
+    children,
+}: {
+    header: any;
+    headerNoStyling?: boolean;
+    defaultVisible: boolean;
+    className?: string;
+    objectVisible?: boolean;
+    children: React.ReactNode;
+}) {
+    const [visible, setVisible] = useState<boolean>(defaultVisible);
+
+    return (
+        <Card
+            className={className}
+            headerNoStyling={headerNoStyling}
+            bodyVisible={visible}
+            objectVisible={objectVisible}
+            header={
+                <>
+                    {header}
+                    <button
+                        className="btn btn-outline-secondary border-0 ms-1"
+                        onClick={() => setVisible((old) => !old)}
+                    >
+                        <i className={"bi bi-chevron-" + (visible ? "down" : "up")} />
+                    </button>
+                </>
+            }
+        >
+            {children}
+        </Card>
+    );
+}
diff --git a/src/videoag/miscellaneous/index.ts b/src/videoag/miscellaneous/index.ts
index d903cd63f3f839abbf0b56af378ae458121301a7..e6bf4ceaa92690c6c2ffc3387a74d217ac44314f 100644
--- a/src/videoag/miscellaneous/index.ts
+++ b/src/videoag/miscellaneous/index.ts
@@ -8,6 +8,7 @@ export {
     datetimeToString,
     filesizeToHuman,
 } from "./Formatting";
+export { Card, CollapsableCard } from "./Card";
 export {
     useDebounce,
     useDebounceWithArgument,
diff --git a/src/videoag/object_management/OMConfigComponent.tsx b/src/videoag/object_management/OMConfigComponent.tsx
index 930a0119c92667107a39ab0816c30c4f9d7f8072..d8de3fffaa8e4a1c677685bb6d6a06d126db2811 100644
--- a/src/videoag/object_management/OMConfigComponent.tsx
+++ b/src/videoag/object_management/OMConfigComponent.tsx
@@ -308,7 +308,7 @@ export function EmbeddedOMFieldComponent({
             className={
                 "h-auto d-inline-flex align-middle" +
                 className +
-                ` ${isEditing ? "w-100" : ""} ${changeIndicator === "background" ? "p-2 rounded" : ""}`
+                ` ${isEditing && field_type !== "datetime" ? "w-100" : ""} ${changeIndicator === "background" ? "p-2 rounded" : ""}`
             }
             style={{
                 backgroundColor: