diff --git a/BewerbungWebseite/Models/SiteContent/ContentLimits.cs b/BewerbungWebseite/Models/SiteContent/ContentLimits.cs
new file mode 100644
index 0000000..1f95d4b
--- /dev/null
+++ b/BewerbungWebseite/Models/SiteContent/ContentLimits.cs
@@ -0,0 +1,83 @@
+namespace BewerbungsSeite.Models.SiteContent;
+
+///
+/// Einzige Quelle der Wahrheit fuer die Laenge der CMS-Textfelder. Jede Zahl
+/// steht hier genau einmal und wird von drei Stellen verwendet:
+/// - den [StringLength]-Attributen auf den SiteContent-Models (harte
+/// Grenze, die ein manipulierter Request nicht umgehen kann),
+/// - den data-max-Attributen, die EditMarkup im Admin-Editor rendert,
+/// - dem Live-Zaehler in wwwroot/js/page-editor.js, der data-max ausliest.
+/// Ändert sich eine Grenze, reicht die Aenderung an genau dieser Zahl.
+///
+/// Die Werte sind nicht pro einzelnem Feld gewaehlt, sondern pro Feld-Typ:
+/// viele Felder teilen denselben Zweck (z. B. alle "kicker"-Felder) und
+/// damit denselben verfuegbaren Platz im Layout. Herleitung siehe die
+/// Analyse-Tabelle in der Aufgabenbeschreibung; kurz zusammengefasst:
+/// - Kicker/Titel/Buttons/Icons sitzen einzeilig neben anderen Elementen
+/// -> enge, harte Grenzen.
+/// - Fliesstexte (Intro, Beschreibungen) haben viel Breite und brechen
+/// dank CSS sauber um -> grosszuegiger Deckel, kein hartes Sperren beim
+/// Tippen im Editor.
+///
+public static class ContentLimits
+{
+ /// Kicker-Label ueber einem Section-Titel (alle 8 Sections).
+ public const int Kicker = 40;
+
+ /// Grosser Section-/Hero-Titel bzw. dessen hervorgehobener Teil.
+ public const int Heading = 50;
+
+ /// Titel einer Skill-/Ziel-Kachel.
+ public const int TileTitle = 50;
+
+ ///
+ /// Text einer Skill-/Ziel-Kachel. Kacheln stehen im selben Flex-Row-Verbund
+ /// wie ihre Nachbarn und werden dadurch alle gleich hoch -- deshalb der
+ /// engste Deckel unter den Fliesstext-Feldern.
+ ///
+ public const int TileText = 180;
+
+ /// Label einer Kurzfakten-Zeile (about.facts.label).
+ public const int FactLabel = 30;
+
+ /// Wert einer Kurzfakten-Zeile (about.facts.value).
+ public const int FactValue = 80;
+
+ /// CSS-Klasse eines Icons (kein Freitext).
+ public const int IconClass = 60;
+
+ /// Titel einer Projekt-/Erfahrungs-/Dokumentkarte.
+ public const int CardTitle = 70;
+
+ /// Kleiner Zwischentitel innerhalb einer Karte (z. B. "TECH:").
+ public const int DetailHeading = 50;
+
+ /// Kurze Meta-Zeile unter einem Kartentitel (Experience.meta).
+ public const int Meta = 80;
+
+ /// Beschriftung eines Buttons oder Links.
+ public const int ButtonLabel = 40;
+
+ ///
+ /// Fliesstext in einer breiten Spalte (Hero-Intro, About-Text,
+ /// Projekt-/Detail-/Erfahrungstext, Dokumentbeschreibung). Grosszuegiger
+ /// Deckel als Rueckfallgrenze -- im Editor nur Warnfarbe, kein Sperren.
+ ///
+ public const int ParagraphText = 500;
+
+ /// Zweizeilige Einleitung ueber dem Kontaktformular.
+ public const int FormIntro = 200;
+
+ /// Url, Datei- oder Bildpfad. Kein Layoutrisiko, nur Schutz vor Missbrauch.
+ public const int Url = 300;
+
+ /// Alt-Text eines Bildes. Unsichtbar, nur Hygiene-Deckel.
+ public const int AltText = 200;
+
+ ///
+ /// Deutsche Fehlermeldung fuer alle [StringLength]-Attribute unten (der
+ /// ASP.NET-Standardtext ist Englisch). {1} fuellt ASP.NET Core automatisch
+ /// mit der maximalen Laenge des jeweiligen Attributs.
+ ///
+ public const string TooLongMessage = "Text ist zu lang (maximal {1} Zeichen erlaubt).";
+}
diff --git a/BewerbungWebseite/Models/SiteContent/PageContent.cs b/BewerbungWebseite/Models/SiteContent/PageContent.cs
new file mode 100644
index 0000000..1e140f6
--- /dev/null
+++ b/BewerbungWebseite/Models/SiteContent/PageContent.cs
@@ -0,0 +1,41 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace BewerbungsSeite.Models.SiteContent;
+
+///
+/// Wurzelobjekt des gesamten Seiteninhalts. Wird als JSON serialisiert und
+/// in einer einzigen Zeile der Tabelle PageContent abgelegt.
+/// Neue Bereiche kommen als zusaetzliche Property dazu; alte Datensaetze
+/// kennen die Property noch nicht und erhalten beim Laden den Initialwert.
+///
+public class PageContent
+{
+ public HeroSection Hero { get; set; } = new();
+ public AboutSection About { get; set; } = new();
+ public SkillsSection Skills { get; set; } = new();
+ public ProjectsSection Projects { get; set; } = new();
+ public GoalsSection Goals { get; set; } = new();
+ public ExperienceSection Experience { get; set; } = new();
+ public DocumentsSection Documents { get; set; } = new();
+ public ContactSection Contact { get; set; } = new();
+}
+
+/// Ein Link mit Beschriftung, z. B. in der Hero-Section oder auf einer Projektkarte.
+public class LinkItem
+{
+ [StringLength(ContentLimits.ButtonLabel, ErrorMessage = ContentLimits.TooLongMessage)]
+ public string Text { get; set; } = string.Empty;
+
+ [StringLength(ContentLimits.Url, ErrorMessage = ContentLimits.TooLongMessage)]
+ public string Url { get; set; } = string.Empty;
+}
+
+/// Ein Abschnitt aus Zwischentitel und Text, z. B. "TECH:" auf einer Projektkarte.
+public class DetailBlock
+{
+ [StringLength(ContentLimits.DetailHeading, ErrorMessage = ContentLimits.TooLongMessage)]
+ public string Heading { get; set; } = string.Empty;
+
+ [StringLength(ContentLimits.ParagraphText, ErrorMessage = ContentLimits.TooLongMessage)]
+ public string Text { get; set; } = string.Empty;
+}