diff --git a/aleksis/apps/kort/admin.py b/aleksis/apps/kort/admin.py
new file mode 100644
index 0000000000000000000000000000000000000000..9c4a2ff4f10e7707226344aa6e847941da73d984
--- /dev/null
+++ b/aleksis/apps/kort/admin.py
@@ -0,0 +1,7 @@
+from django.contrib import admin
+
+from aleksis.apps.kort.models import Card, CardLayout, CardPrinter
+
+admin.site.register(Card)
+admin.site.register(CardPrinter)
+admin.site.register(CardLayout)
diff --git a/aleksis/apps/kort/migrations/0005_card_pdf_file.py b/aleksis/apps/kort/migrations/0005_card_pdf_file.py
new file mode 100644
index 0000000000000000000000000000000000000000..e78b54c530c0cf80ff17c3e28ac29cde73ca37fb
--- /dev/null
+++ b/aleksis/apps/kort/migrations/0005_card_pdf_file.py
@@ -0,0 +1,19 @@
+# Generated by Django 3.2.12 on 2022-03-10 16:48
+
+import django.core.validators
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('kort', '0004_alter_card_chip_number'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='card',
+            name='pdf_file',
+            field=models.FileField(blank=True, null=True, upload_to='cards/', validators=[django.core.validators.FileExtensionValidator(['pdf'])], verbose_name='PDF file'),
+        ),
+    ]
diff --git a/aleksis/apps/kort/models.py b/aleksis/apps/kort/models.py
index a81d6ed37fecd18dc24b1e58917f163579147a31..e9123d8ae81f12cc3a4ea1ffe987fbb543a49119 100644
--- a/aleksis/apps/kort/models.py
+++ b/aleksis/apps/kort/models.py
@@ -1,9 +1,14 @@
+from typing import Union
+
 from django.core.exceptions import ValidationError
+from django.core.validators import FileExtensionValidator
 from django.db import models
 from django.template import Context, Template
 from django.utils import timezone
 from django.utils.translation import gettext as _
 
+from celery.result import AsyncResult
+
 from aleksis.core.mixins import ExtensibleModel
 from aleksis.core.models import Person
 
@@ -69,6 +74,14 @@ class Card(ExtensibleModel):
     valid_until = models.DateField(verbose_name=_("Valid until"))
     deactivated = models.BooleanField(verbose_name=_("Deactivated"), default=False)
 
+    pdf_file = models.FileField(
+        verbose_name=_("PDF file"),
+        blank=True,
+        null=True,
+        upload_to="cards/",
+        validators=[FileExtensionValidator(["pdf"])],
+    )
+
     @property
     def is_valid(self):
         return (
@@ -86,6 +99,18 @@ class Card(ExtensibleModel):
             "valid_until": self.valid_until,
         }
 
+    def generate_pdf(self) -> Union[bool, AsyncResult]:
+        from .tasks import generate_card_pdf
+
+        if self.pdf_file:
+            return True
+        return generate_card_pdf.delay(self.pk)
+
+    def __str__(self):
+        if self.chip_number:
+            return f"{self.person} ({self.chip_number})"
+        return f"{self.person}"
+
     class Meta:
         verbose_name = _("Card")
         verbose_name_plural = _("Cards")
diff --git a/aleksis/apps/kort/settings.py b/aleksis/apps/kort/settings.py
new file mode 100644
index 0000000000000000000000000000000000000000..1cbd74e3786c9c49e0f88b35a205ac4467d658d5
--- /dev/null
+++ b/aleksis/apps/kort/settings.py
@@ -0,0 +1,7 @@
+from aleksis.core.settings import JS_URL
+
+YARN_INSTALLED_APPS = ["pdfobject"]
+
+ANY_JS = {
+    "pdfobject": {"js_url": JS_URL + "/pdfobject/pdfobject.min.js"},
+}
diff --git a/aleksis/apps/kort/tables.py b/aleksis/apps/kort/tables.py
index 635102ccf613f0bca82da4ab8b39077bb36bfb3e..5b53edde95c511f73605f25b3adc094ee3b674e6 100644
--- a/aleksis/apps/kort/tables.py
+++ b/aleksis/apps/kort/tables.py
@@ -1,7 +1,7 @@
 from django.template.loader import render_to_string
 from django.utils.translation import gettext as _
 
-from django_tables2 import A, BooleanColumn, Column, RelatedLinkColumn, Table
+from django_tables2 import A, BooleanColumn, Column, LinkColumn, RelatedLinkColumn, Table
 
 
 class CardTable(Table):
@@ -11,7 +11,7 @@ class CardTable(Table):
         attrs = {"class": "highlight"}
 
     person = RelatedLinkColumn()
-    chip_number = Column(verbose_name=_("Chip number"))
+    chip_number = LinkColumn("card", verbose_name=_("Chip number"), args=[A("pk")])
     current_status = Column(verbose_name=_("Current status"), accessor=A("pk"))
     valid_until = Column(verbose_name=_("Valid until"))
     deactivated = BooleanColumn(verbose_name=_("Deactivated"))
@@ -19,10 +19,9 @@ class CardTable(Table):
 
     def render_current_status(self, value, record):
         return render_to_string(
-            "components/materialize-chips.html",
+            "kort/card/status.html",
             dict(
-                content=_("Valid") if record.is_valid else _("Not valid"),
-                classes="white-text " + ("green" if record.is_valid else "red"),
+                card=record,
             ),
         )
 
diff --git a/aleksis/apps/kort/tasks.py b/aleksis/apps/kort/tasks.py
new file mode 100644
index 0000000000000000000000000000000000000000..82beaa0b3d7b51f95a82eaadecad6c4aa0d0f01d
--- /dev/null
+++ b/aleksis/apps/kort/tasks.py
@@ -0,0 +1,21 @@
+from celery.result import allow_join_result
+from celery.states import SUCCESS
+
+from aleksis.apps.kort.models import Card
+from aleksis.core.celery import app
+from aleksis.core.util.pdf import generate_pdf_from_template
+
+
+@app.task
+def generate_card_pdf(card_pk: int):
+    card = Card.objects.get(pk=card_pk)
+
+    context = card.get_context()
+    file_object, result = generate_pdf_from_template("kort/pdf.html", context)
+
+    with allow_join_result():
+        result.wait()
+        file_object.refresh_from_db()
+        if result.status == SUCCESS and file_object.file:
+            card.pdf_file.save("card.pdf", file_object.file.file)
+            card.save()
diff --git a/aleksis/apps/kort/templates/kort/card/actions.html b/aleksis/apps/kort/templates/kort/card/actions.html
index 1c39b5daffc6cc26d456c1d4c05c44f97214bde7..ce253a92178bb47677c286fdf3d106a9d23b72e7 100644
--- a/aleksis/apps/kort/templates/kort/card/actions.html
+++ b/aleksis/apps/kort/templates/kort/card/actions.html
@@ -1,50 +1,44 @@
 {% load i18n %}
-<!-- Modal Structure -->
-<div id="deactivate-modal-{{ card.pk }}" class="modal">
+
+<div id="detail-modal-{{ card.pk }}" class="modal">
   <div class="modal-content">
-    <h4>{% trans "Do you really want to deactivate the following card?" %}</h4>
-    {% include "kort/card/short.html" %}
+    <h4>{% blocktrans with person=card.person %}Card of {{ person }}{% endblocktrans %}</h4>
+    {% include "kort/card/detail_content.html" %}
   </div>
   <div class="modal-footer">
     <a href="#!" class="modal-close waves-effect waves-green btn-flat">
       <i class="material-icons left">close</i>
       {% trans "Close" %}
     </a>
-    <a href="{% url "deactivate_card" card.pk %}" class="modal-close waves-effect waves-light orange btn">
-      <i class="material-icons left">timer_off</i>
-      {% trans "Deactivate" %}
-    </a>
-  </div>
-</div>
-<div id="delete-modal-{{ card.pk }}" class="modal">
-  <div class="modal-content">
-    <h4>{% trans "Do you really want to delete the following card?" %}</h4>
-    {% include "kort/card/short.html" %}
-    <figure class="alert warning">
-      <i class="material-icons left">warning</i>
-      {% blocktrans %}
-        Please pay attention that a deletion of a card is irreversible and should be only used to clean up misprints.
-        If you just want to make a card unusable because a student has lost his card or left the school,
-        please deactivate the card instead.
-      {% endblocktrans %}
-    </figure>
-  </div>
-  <div class="modal-footer">
-    <a href="#!" class="modal-close waves-effect waves-green btn-flat">
-      <i class="material-icons left">close</i>
-      {% trans "Close" %}
-    </a>
-    <a href="{% url "delete_card" card.pk %}" class="modal-close waves-effect waves-light red btn">
-      <i class="material-icons left">delete</i>
-      {% trans "Delete" %}
-    </a>
   </div>
 </div>
-<a class="btn-flat waves-effect waves-orange orange-text modal-trigger" href="#deactivate-modal-{{ card.pk }}">
-  <i class="material-icons left">timer_off</i>
-  {% trans "Deactivate" %}
+<a class="btn-flat waves-effect waves-green green-text modal-trigger" href="#detail-modal-{{ card.pk }}">
+  <i class="material-icons left">slideshow</i>
+  {% trans "Show" %}
 </a>
-<a class="btn-flat waves-effect waves-red red-text modal-trigger" href="#delete-modal-{{ card.pk }}">
+{% if not card.deactivated %}
+  <div id="deactivate-modal-{{ card.pk }}" class="modal">
+    <div class="modal-content">
+      <h4>{% trans "Do you really want to deactivate the following card?" %}</h4>
+      {% include "kort/card/short.html" %}
+    </div>
+    <div class="modal-footer">
+      <a href="#!" class="modal-close waves-effect waves-green btn-flat">
+        <i class="material-icons left">close</i>
+        {% trans "Close" %}
+      </a>
+      <a href="{% url "deactivate_card" card.pk %}" class="modal-close waves-effect waves-light orange btn">
+        <i class="material-icons left">timer_off</i>
+        {% trans "Deactivate" %}
+      </a>
+    </div>
+  </div>
+  <a class="btn-flat waves-effect waves-orange orange-text modal-trigger" href="#deactivate-modal-{{ card.pk }}">
+    <i class="material-icons left">timer_off</i>
+    {% trans "Deactivate" %}
+  </a>
+{% endif %}
+<a class="btn-flat waves-effect waves-red red-text modal-trigger" href="{% url "delete_card" card.pk %}">
   <i class="material-icons left">delete</i>
   {% trans "Delete" %}
 </a>
\ No newline at end of file
diff --git a/aleksis/apps/kort/templates/kort/card/detail.html b/aleksis/apps/kort/templates/kort/card/detail.html
new file mode 100644
index 0000000000000000000000000000000000000000..d5176cacc78154bf71b82091819a19fbf5f9a27f
--- /dev/null
+++ b/aleksis/apps/kort/templates/kort/card/detail.html
@@ -0,0 +1,15 @@
+{# -*- engine:django -*- #}
+
+{% extends "core/base.html" %}
+
+{% load material_form i18n any_js %}
+
+
+{% block browser_title %}{% blocktrans %}Card{% endblocktrans %}{% endblock %}
+{% block page_title %}{% blocktrans with person=object.person %}Card of {{ person }}{% endblocktrans %}{% endblock %}
+
+
+{% block content %}
+  {% include_js "pdfobject" %}
+  {% include "kort/card/detail_content.html" with card=object %}
+{% endblock %}
diff --git a/aleksis/apps/kort/templates/kort/card/detail_content.html b/aleksis/apps/kort/templates/kort/card/detail_content.html
new file mode 100644
index 0000000000000000000000000000000000000000..c0a4cb37836d6a09ea6b854ed0ec51200f94f0d2
--- /dev/null
+++ b/aleksis/apps/kort/templates/kort/card/detail_content.html
@@ -0,0 +1,43 @@
+{% load i18n %}
+<div class="row">
+  <div class="col s12 m12 l6">
+    <div class="card">
+      <div class="card-content">
+        <div class="card-title">{% trans "Card details" %}</div>
+        <table>
+          <tr>
+            <th>{% trans "Person" %}</th>
+            <td>{{ card.person }}</td>
+          </tr>
+          <tr>
+            <th>{% trans "Chip number" %}</th>
+            <td><code>{{ card.chip_number|default:_("not set yet") }}</code></td>
+          </tr>
+          <tr>
+            <th>{% trans "Valid until" %}</th>
+            <td>{{ card.valid_until }}</td>
+          </tr>
+          <tr>
+            <th>{% trans "Status" %}</th>
+            <td>
+              {% include "kort/card/status.html" %}
+            </td>
+          </tr>
+        </table>
+      </div>
+    </div>
+  </div>
+  <div class="col s12 m12 l6">
+    {% if card.pdf_file %}
+      <div id="card-pdf-{{ card.pk }}" style="height: 500px;"></div>
+      <script>PDFObject.embed("{{ card.pdf_file.url }}", "#card-pdf-{{ card.pk }}");</script>
+    {% else %}
+      <div class="row center-via-flex">
+        <a class="btn waves-effect waves-light" href="{% url "generate_card_pdf" card.pk %}">
+          <i class="material-icons left">picture_as_pdf</i>
+          {% trans "Generate card as PDF" %}
+        </a>
+      </div>
+    {% endif %}
+  </div>
+</div>
\ No newline at end of file
diff --git a/aleksis/apps/kort/templates/kort/card/list.html b/aleksis/apps/kort/templates/kort/card/list.html
index 2a6524b30b7ea87dd108806c21ce27360e2011ea..874d37d17bc8ad80f6d954d22ea47e2ea33c4c4c 100644
--- a/aleksis/apps/kort/templates/kort/card/list.html
+++ b/aleksis/apps/kort/templates/kort/card/list.html
@@ -2,7 +2,7 @@
 
 {% extends "core/base.html" %}
 
-{% load i18n rules material_form %}
+{% load i18n rules material_form any_js %}
 {% load render_table from django_tables2 %}
 
 {% block browser_title %}{% blocktrans %}Cards{% endblocktrans %}{% endblock %}
@@ -36,5 +36,6 @@
   {#  </form>#}
 
   {#  <h2>{% trans "Selected persons" %}</h2>#}
+  {% include_js "pdfobject" %}
   {% render_table table %}
 {% endblock %}
diff --git a/aleksis/apps/kort/templates/kort/card/status.html b/aleksis/apps/kort/templates/kort/card/status.html
new file mode 100644
index 0000000000000000000000000000000000000000..009baa0bcdea5fbd575d481ae9f2a0efcf9bd7af
--- /dev/null
+++ b/aleksis/apps/kort/templates/kort/card/status.html
@@ -0,0 +1,6 @@
+{% load i18n %}
+{% if card.is_valid %}
+  <span class="badge new green white-text">{% trans "Valid" %}</span>
+{% else %}
+  <span class="badge new green white-text">{% trans "Not valid" %}</span>
+{% endif %}
\ No newline at end of file
diff --git a/aleksis/apps/kort/templates/kort/pdf.html b/aleksis/apps/kort/templates/kort/pdf.html
index a1d51ba1d409bb86750611309afbaec0c2f9cc98..c01cdc31131162fef0d57f562d9e58c88ccb7c4b 100644
--- a/aleksis/apps/kort/templates/kort/pdf.html
+++ b/aleksis/apps/kort/templates/kort/pdf.html
@@ -181,7 +181,7 @@
             Name/Surname/Nom
           </div>
           <div class="info-value">
-            Mustermann
+            {{ person.last_name }}
           </div>
         </div>
         <div class="info-item">
@@ -189,7 +189,7 @@
             Vornamen/Given names/Prénoms
           </div>
           <div class="info-value">
-            Max
+            {{ person.first_name }} {{ person.additional_name }}
           </div>
         </div>
         <div class="info-item">
@@ -197,7 +197,7 @@
             Wohnort/Residence/Résidence
           </div>
           <div class="info-value">
-            Musterstraße 1, 12345 Musterstadt
+            {{ person.street }} {{ person.housenumber }}, {{ person.postal_code }} {{ person.place }}
           </div>
         </div>
         <div class="info-item">
@@ -205,14 +205,14 @@
             Geburtsdatum/Date of birth/Date de naissance
           </div>
           <div class="info-value">
-            01.01.2007
+            {{ person.date_of_birth|date:"SHORT_DATE_FORMAT" }}
           </div>
         </div>
 
       </div>
     </div>
     <div class="front-footer">
-      {% generate_barcode "55846268859" %}
+      {% generate_barcode chip_number %}
       <div class="signature">
         Lübeck, den {% now "SHORT_DATE_FORMAT" %}
         <img src="{% static "kort/signature.png" %}">
diff --git a/aleksis/apps/kort/urls.py b/aleksis/apps/kort/urls.py
index 8aa00d73f70c3c65d37b862b93d9c9b75f52db55..983140bc22fee78ee81eddfa486a3ca36faffb92 100644
--- a/aleksis/apps/kort/urls.py
+++ b/aleksis/apps/kort/urls.py
@@ -6,6 +6,12 @@ urlpatterns = [
     path("test", views.TestPDFView.as_view(), name="test_pdf"),
     path("cards/", views.CardListView.as_view(), name="cards"),
     path("cards/create/", views.CardCreateView.as_view(), name="create_card"),
+    path("cards/<int:pk>/", views.CardDetailView.as_view(), name="card"),
+    path(
+        "cards/<int:pk>/generate_pdf/",
+        views.CardGeneratePDFView.as_view(),
+        name="generate_card_pdf",
+    ),
     path("cards/<int:pk>/deactivate/", views.CardDeactivateView.as_view(), name="deactivate_card"),
     path("cards/<int:pk>/delete/", views.CardDeleteView.as_view(), name="delete_card"),
 ]
diff --git a/aleksis/apps/kort/views.py b/aleksis/apps/kort/views.py
index bfe584c9663db438e57725eb1fb1488ec92a24a4..0c088a9fa89b4f0a19c905782c623f44c8234319 100644
--- a/aleksis/apps/kort/views.py
+++ b/aleksis/apps/kort/views.py
@@ -1,10 +1,10 @@
 from django.contrib import messages
 from django.http import HttpRequest, HttpResponse
 from django.shortcuts import redirect, render
-from django.urls import reverse_lazy
+from django.urls import reverse, reverse_lazy
 from django.utils.translation import gettext as _
 from django.views import View
-from django.views.generic.detail import SingleObjectMixin
+from django.views.generic.detail import DetailView, SingleObjectMixin
 
 from django_tables2 import SingleTableView
 from reversion.views import RevisionMixin
@@ -14,6 +14,7 @@ from aleksis.apps.kort.forms import CardForm
 from aleksis.apps.kort.models import Card
 from aleksis.apps.kort.tables import CardTable
 from aleksis.core.mixins import AdvancedCreateView, AdvancedDeleteView
+from aleksis.core.util.celery_progress import render_progress_page
 from aleksis.core.views import RenderPDFView
 
 
@@ -72,3 +73,40 @@ class CardDeactivateView(PermissionRequiredMixin, RevisionMixin, SingleObjectMix
         self.object.deactivate()
         messages.success(request, _("The card has been deactivated successfully."))
         return redirect(self.success_url)
+
+
+class CardDetailView(PermissionRequiredMixin, RevisionMixin, DetailView):
+    permission_required = "core.view_card_rule"
+    model = Card
+    template_name = "kort/card/detail.html"
+
+
+class CardGeneratePDFView(PermissionRequiredMixin, RevisionMixin, SingleObjectMixin, View):
+    permission_required = "views.view_card_rule"
+    model = Card
+
+    def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
+        self.object = self.get_object()
+
+        if not self.object.chip_number:
+            messages.error(request, _("The chip number is missing."))
+            return redirect("cards")
+
+        redirect_url = reverse("card", args=[self.object.pk])
+        result = self.object.generate_pdf()
+
+        if result == True:
+            return redirect(redirect_url)
+
+        return render_progress_page(
+            request,
+            result,
+            title=_("Progress: Generate card layout as PDF file"),
+            progress_title=_("Generating PDF file …"),
+            success_message=_("The PDF file with the card layout has been generated successfully."),
+            error_message=_("There was a problem while generating the PDF file."),
+            redirect_on_success_url=redirect_url,
+            button_title=_("Show card"),
+            button_url=redirect_url,
+            button_icon="credit_card",
+        )