Eine Formensprache statt vier Radien und sechs Schriftgrössen

Die Oberfläche wirkte zusammengesetzt, nicht entworfen. Vier Gründe, alle
am Quelltext belegbar.

Die Farben waren von der App abgedriftet: elevated, placeholder, text,
text-dim, text-muted und die Trennlinie wichen in der letzten Stelle ab,
und der Standardakzent war in der App Lilac, im Browser Orchid. Einzeln
unsichtbar, zusammen genau der Grund, warum App und Browser nicht wie
dasselbe Programm aussahen. Alles auf FlaroDesign zurückgeführt.

Vier konkurrierende Radien — 6 auf Plakaten, 8 auf Karten, 999 auf jedem
Knopf, zweimal nacktes 3 — sind jetzt drei, und die ersten beiden stimmen
wieder mit FlaroDesign überein. Pillen bleiben, wo sie hingehören: Suche
und Filter.

Sechs Überschriftengrössen ohne Verhältnis zueinander sind eine Skala.
Der Kacheltitel war mit 13 px/500 leiser als die Metazeile daneben; das
erzeugt keine Hierarchie, sondern Rauschen.

Und der Akzent sass auf neun Dingen gleichzeitig. Wenn alles Akzent ist,
markiert er nichts. Der Primärknopf ist jetzt hell mit dunkler Schrift —
genau das macht Netflix, dessen Rot nur im Schriftzug vorkommt.

Dazu drei Dinge, die vorher ganz fehlten: ein sichtbarer Fokusring auf
allem Bedienbaren, ohne den die Oberfläche mit der Tastatur praktisch
unbedienbar war; Rücksicht auf "Bewegung reduzieren", das im Projekt
nirgends vorkam; und ein Hover, der mehr tut als zwei Bildpunkte zu
heben — mitsamt einem Abspielknopf, der nicht länger das halbe Plakat
verdeckt.

Tools/tokens-pruefen.py vergleicht beide Quellen und läuft im
Veröffentlichungsskript mit. Kein Generator: erzeugter Swift liest sich
schlechter als geschriebener, und die Prüfung fängt dasselbe ab.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-04 12:31:00 +02:00
co-authored by Claude Opus 5
parent 0cd88acf70
commit 6bd9cdb12d
4 changed files with 310 additions and 37 deletions
+7
View File
@@ -5,6 +5,13 @@
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="theme-color" content="#0B0B0D">
<title>Flaro</title>
<!-- **Inter, mit der Systemschrift als Rückfall.** `display=swap`, damit der
Text sofort steht und nicht erst, wenn die Schrift da ist — ein
Heimserver im eigenen Netz soll nicht auf Google warten. -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap">
</head>
<body>
<section id="login"></section>
+151 -37
View File
@@ -9,27 +9,81 @@
* Vorbild ist die Anordnung, nicht das Aussehen. */
:root {
--accent: #C04AC9;
--accent-dim: #8E3595;
--accent-soft: rgba(192, 74, 201, .16);
/* --- Akzent ------------------------------------------------------------
*
* **Lilac, nicht Orchidee.** Die Apps starten ab Werk mit `#9B8AD4`
* (`AccentOption.swift`), der Web-Client stand auf `#C04AC9`. Dieselbe
* Software in zwei Farben ist keine Gestaltung, sondern Drift.
*
* Die beiden abgeleiteten Werte werden **gerechnet**, nicht gepflegt —
* sonst müsste man sie bei jedem Farbwechsel von Hand mitziehen und
* vergisst es beim zweiten Mal. */
--accent: #9B8AD4;
--accent-dim: color-mix(in srgb, var(--accent) 72%, black);
--accent-soft: color-mix(in srgb, var(--accent) 16%, transparent);
/* --- Flächen -----------------------------------------------------------
*
* Auf FlaroDesign zurückgeführt: `elevated`, `placeholder`, `text`,
* `text-dim`, `text-muted` und die Trennlinie wichen in der letzten
* Stelle ab — unsichtbar einzeln, zusammen aber genau der Grund, warum
* App und Browser nicht wie dasselbe Programm aussahen. */
--canvas: #0B0B0D;
--surface: #141417;
--elevated: #1D1D22;
--raised: #26262C;
--placeholder: #202026;
--line: rgba(255, 255, 255, .08);
--elevated: #1D1D21;
--raised: #26262B;
--placeholder: #26262B;
--line: rgba(255, 255, 255, .10);
--text: #F2F2F5;
--text-dim: #B4B4BE;
--text-muted: #8A8A95;
--text-faint: #65656F;
/* Kopf- und Seitenleiste. **Eigenes Token, weil es vorher als `#0E0E11`
* hart im Regelwerk stand** — und sich damit jedem Theme entzog. */
--chrome: #0E0E11;
--text: #F5F5F7;
--text-dim: #A0A0A8;
--text-muted: #6E6E76;
--text-faint: #55555C;
--destructive: #E5484D;
--destructive-dim: color-mix(in srgb, var(--destructive) 78%, black);
/* Schatten als Token, nicht fünfmal fest verdrahtet. */
--schatten-karte: 0 8px 24px rgba(0, 0, 0, .55);
--schatten-plakat: 0 12px 40px rgba(0, 0, 0, .6);
--schatten-dialog: 0 24px 64px rgba(0, 0, 0, .7);
--s-tight: 4px; --s-snug: 8px; --s-regular: 12px;
--s-loose: 16px; --s-section: 24px; --s-page: 32px;
--r-control: 8px; --r-card: 6px; --r-pill: 999px;
/* --- Formen ------------------------------------------------------------
*
* **Drei Radien statt vier, und keiner mehr zufällig.** Vorher: 6 px auf
* Plakaten, 8 px auf Karten und Feldern, 999 px auf *jedem* Knopf, dazu
* zweimal nacktes 3 px. Vollrunde Primärknöpfe neben 8-px-Karten ergeben
* keine Formensprache — und sie sind der zweitstärkste Hinweis darauf,
* dass eine Oberfläche nicht entworfen, sondern zusammengesetzt wurde.
*
* 8 und 10 sind dieselben Werte wie in FlaroDesign. */
--r-karte: 8px;
--r-flaeche: 10px;
--r-knopf: 6px;
--r-pill: 999px;
/* Rückwärtsnamen, solange noch Regeln daran hängen. */
--r-card: var(--r-karte);
--r-control: var(--r-flaeche);
/* --- Schrift -----------------------------------------------------------
*
* **Eine Skala statt sechs Zufallswerten.** Die Überschriften standen auf
* 34 / 26 / 22 / 20 / 19 / 17 px — Werte ohne Verhältnis zueinander, alle
* in 700, mit fast gleichem negativem Spacing. Das erzeugt keine
* Hierarchie, sondern Rauschen. */
--t-hero: 32px;
--t-titel: 24px;
--t-abschnitt: 19px;
--t-gross: 15px;
--t-normal: 13px;
--t-klein: 11px;
--sidebar-w: 240px;
--topbar-h: 56px;
@@ -45,7 +99,7 @@ body {
margin: 0;
background: var(--canvas);
color: var(--text);
font: 400 14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font: 400 14px/1.55 Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
-webkit-font-smoothing: antialiased;
}
@@ -65,11 +119,11 @@ svg { width: 20px; height: 20px; display: block; flex: none; }
position: fixed; inset: 0 0 auto 0; height: var(--topbar-h); z-index: 30;
display: flex; align-items: center; gap: var(--s-loose);
padding: 0 var(--s-loose);
background: #0E0E11;
background: var(--chrome);
border-bottom: 1px solid var(--line);
}
#topbar .marke {
font-size: 20px; font-weight: 700; letter-spacing: -.02em;
font-size: var(--t-abschnitt); font-weight: 700; letter-spacing: -.02em;
color: var(--accent); user-select: none;
}
#topbar .suchfeld {
@@ -101,7 +155,7 @@ svg { width: 20px; height: 20px; display: block; flex: none; }
#chrome {
position: fixed; top: var(--topbar-h); bottom: 0; left: 0;
width: var(--sidebar-w); overflow-y: auto;
background: #0E0E11;
background: var(--chrome);
border-right: 1px solid var(--line);
padding: var(--s-loose) 0;
display: flex; flex-direction: column; gap: 2px;
@@ -160,7 +214,7 @@ svg { width: 20px; height: 20px; display: block; flex: none; }
background: var(--surface); border: 1px solid var(--line);
border-radius: var(--r-control); padding: var(--s-section);
}
.login-card h1 { margin: 0; font-size: 26px; letter-spacing: -.02em; color: var(--accent); }
.login-card h1 { margin: 0; font-size: var(--t-titel); letter-spacing: -.02em; color: var(--accent); }
.bleiben {
display: flex; align-items: center; gap: var(--s-snug);
color: var(--text-dim); font-size: 13px; cursor: pointer; user-select: none;
@@ -174,7 +228,7 @@ svg { width: 20px; height: 20px; display: block; flex: none; }
margin-bottom: var(--s-section);
}
.seitenkopf .wer { min-width: 0; }
.seitenkopf h1 { margin: 0; font-size: 22px; font-weight: 700; letter-spacing: -.02em; }
.seitenkopf h1 { margin: 0; font-size: var(--t-titel); font-weight: 700; letter-spacing: -.02em; }
.seitenkopf .quelle {
font-size: 11px; letter-spacing: .06em; text-transform: uppercase; color: var(--text-faint);
}
@@ -257,8 +311,8 @@ svg { width: 20px; height: 20px; display: block; flex: none; }
.plakat {
position: relative; width: 100%; aspect-ratio: 2 / 3;
background: var(--placeholder);
border-radius: var(--r-card); overflow: hidden;
transition: transform .15s ease, box-shadow .15s ease;
border-radius: var(--r-karte); overflow: hidden;
transition: transform .18s ease-out, box-shadow .18s ease-out;
}
.plakat img {
width: 100%; height: 100%; object-fit: cover;
@@ -266,19 +320,31 @@ svg { width: 20px; height: 20px; display: block; flex: none; }
}
.plakat.geladen img { opacity: 1; }
.kachel:hover .plakat {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(0, 0, 0, .55);
/* **Hover mit Inhalt.** Zwei Bildpunkte Anheben waren kaum zu sehen; ein
leichtes Hochskalieren hebt die Kachel aus der Reihe heraus, so wie bei
Netflix. Nur `transform` und `box-shadow`, damit nichts umbricht. */
.kachel:hover .plakat,
.kachel:focus-visible .plakat {
transform: scale(1.04);
box-shadow: var(--schatten-karte);
z-index: 3;
}
/* Der Abspielknopf erscheint beim Überfahren, mittig. */
/* **Der Abspielknopf als Kreis unten links, nicht als Vollflächen-Overlay.**
Das Overlay legte sich über das ganze Plakat und verdeckte genau das Bild,
nach dem man gerade sucht. */
.schweber {
position: absolute; inset: 0; display: grid; place-items: center;
background: rgba(0, 0, 0, .45);
opacity: 0; transition: opacity .15s ease;
position: absolute; left: var(--s-snug); bottom: var(--s-snug);
display: grid; place-items: center;
width: 38px; height: 38px; border-radius: 50%;
background: rgba(10, 10, 12, .78);
backdrop-filter: blur(6px);
opacity: 0; transform: translateY(6px);
transition: opacity .18s ease-out, transform .18s ease-out;
}
.schweber svg { width: 44px; height: 44px; color: #fff; filter: drop-shadow(0 2px 8px rgba(0,0,0,.6)); }
.kachel:hover .schweber { opacity: 1; }
.schweber svg { width: 20px; height: 20px; color: #fff; }
.kachel:hover .schweber,
.kachel:focus-visible .schweber { opacity: 1; transform: none; }
/* **Haken und Zahl sitzen in derselben Ecke**, weil sie dieselbe Frage
beantworten: ist hier noch etwas offen? */
@@ -302,7 +368,10 @@ svg { width: 20px; height: 20px; display: block; flex: none; }
.kacheltext { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.kacheltext strong {
font-weight: 500; font-size: 13px;
/* **Lauter als die Zeile darunter.** Mit 13 px/500 war der Titel leiser
als manche Metazeile daneben — die Typografie erzeugte damit keine
Hierarchie, sondern Rauschen. */
font-weight: 600; font-size: 14px; color: var(--text);
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.kacheltext span {
@@ -319,7 +388,7 @@ svg { width: 20px; height: 20px; display: block; flex: none; }
display: flex; align-items: center; gap: var(--s-regular);
margin-bottom: var(--s-regular);
}
.reihe-kopf h2 { margin: 0; font-size: 19px; font-weight: 700; letter-spacing: -.01em; }
.reihe-kopf h2 { margin: 0; font-size: var(--t-abschnitt); font-weight: 700; letter-spacing: -.015em; }
.reihe-kopf .mehr {
background: none; border: 0; padding: 0; font-size: 13px; color: var(--accent);
}
@@ -371,7 +440,7 @@ svg { width: 20px; height: 20px; display: block; flex: none; }
max-width: 420px; max-height: 110px; margin-bottom: var(--s-regular);
object-fit: contain; object-position: left bottom; display: block;
}
.detail-text h1 { margin: 0 0 var(--s-snug); font-size: 34px; font-weight: 700; letter-spacing: -.03em; }
.detail-text h1 { margin: 0 0 var(--s-snug); font-size: var(--t-hero); font-weight: 700; letter-spacing: -.03em; }
.detail-text .unter { margin: 0 0 var(--s-regular); font-size: 17px; color: var(--text-dim); }
.eckdaten {
@@ -393,13 +462,21 @@ svg { width: 20px; height: 20px; display: block; flex: none; }
.btn {
display: inline-flex; align-items: center; gap: var(--s-snug);
background: var(--elevated); border: 0; border-radius: var(--r-pill);
background: var(--elevated); border: 0; border-radius: var(--r-knopf);
padding: 9px var(--s-loose); color: var(--text);
}
.btn:hover { background: var(--raised); }
.btn:disabled { opacity: .5; cursor: default; }
.btn svg { width: 18px; height: 18px; }
.btn-primary { background: var(--accent); color: #fff; font-weight: 600; }
/* **Der Primärknopf ist hell, nicht farbig.**
*
* Genau das macht Netflix: der Abspielknopf ist weiss mit schwarzer Schrift,
* die Markenfarbe erscheint nur im Schriftzug. Der Akzent sass hier auf neun
* Dingen gleichzeitig — Marke, Navigation, Knopf, Fortschritt, Plakette,
* Stern, Fokus, Filter, Kontokreis. Wenn alles Akzent ist, markiert er
* nichts. */
.btn-primary { background: var(--text); color: var(--canvas); font-weight: 600; }
.btn-primary:hover { background: #fff; }
.btn-primary:hover { background: var(--accent-dim); }
/* Runde Knöpfe für alles ohne Beschriftung — wie Plex' Merkliste und Haken. */
@@ -442,8 +519,8 @@ svg { width: 20px; height: 20px; display: block; flex: none; }
/* ---------- Kleinteile ---------- */
h2 { font-size: 19px; font-weight: 700; letter-spacing: -.01em; margin: 0 0 var(--s-regular); }
.seitentitel { margin: 0; font-size: 22px; font-weight: 700; letter-spacing: -.02em; }
h2 { font-size: var(--t-abschnitt); font-weight: 700; letter-spacing: -.015em; margin: 0 0 var(--s-regular); }
.seitentitel { margin: 0; font-size: var(--t-titel); font-weight: 700; letter-spacing: -.02em; }
.einstellungen { display: flex; flex-direction: column; gap: var(--s-regular); margin-bottom: var(--s-page); }
.einstellung {
@@ -541,7 +618,7 @@ h2 { font-size: 19px; font-weight: 700; letter-spacing: -.01em; margin: 0 0 var(
}
.btn-gefahr { background: var(--destructive); color: #fff; font-weight: 600; }
.btn-gefahr:hover { background: #C53338; }
.btn-gefahr:hover { background: var(--destructive-dim); }
/* Der Spieler über allem. */
.pl-fill {
@@ -656,3 +733,40 @@ h2 { font-size: 19px; font-weight: 700; letter-spacing: -.01em; margin: 0 0 var(
font-size: 15px;
color: var(--text-muted);
}
/* --- Fokus und Bewegung --------------------------------------------------
*
* **Ein sichtbarer Fokusring auf allem, was man bedienen kann.** Vorher
* färbte sich allenfalls ein Feldrahmen; mit der Tastatur war die Oberfläche
* praktisch unbedienbar, weil man nicht sah, wo man gerade ist. */
:where(button, a, input, select, textarea, [tabindex]):focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
border-radius: var(--r-knopf);
}
.kachel:focus-visible { outline: none; }
.kachel:focus-visible .plakat {
outline: 2px solid var(--accent);
outline-offset: 3px;
}
/* **Wer „Bewegung reduzieren" eingestellt hat, meint es.**
*
* Das gilt im ganzen Projekt bisher nirgends. Animationen ganz abzuschalten
* wäre falsch — ein Zustandswechsel ohne jede Rückmeldung wirkt wie ein
* Aussetzer. Auf einen Hauch zu kürzen behält die Rückmeldung und nimmt die
* Bewegung. */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: .01ms !important;
animation-iteration-count: 1 !important;
transition-duration: .01ms !important;
scroll-behavior: auto !important;
}
.kachel:hover .plakat,
.kachel:focus-visible .plakat { transform: none; }
}