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:
co-authored by
Claude Opus 5
parent
0cd88acf70
commit
6bd9cdb12d
@@ -43,6 +43,16 @@ if ! grep -q "$REGISTRY" ~/.docker/config.json 2>/dev/null; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# **Vor dem Bauen, nicht danach.** Laufen App und Browser farblich
|
||||
# auseinander, soll das auffallen, bevor ein Image daraus wird.
|
||||
if command -v python3 >/dev/null && [ -f Tools/tokens-pruefen.py ]; then
|
||||
python3 Tools/tokens-pruefen.py || {
|
||||
echo
|
||||
echo "Abgebrochen. Mit FLARO_DRIFT_EGAL=1 trotzdem bauen."
|
||||
[ "${FLARO_DRIFT_EGAL:-}" = "1" ] || exit 1
|
||||
}
|
||||
fi
|
||||
|
||||
MARKEN=(-t "$BILD:latest")
|
||||
[ -n "$FASSUNG" ] && MARKEN+=(-t "$BILD:$FASSUNG")
|
||||
|
||||
|
||||
Executable
+142
@@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Prüft, ob App und Weboberfläche dieselben Farben und Formen benutzen.
|
||||
|
||||
**Warum es das braucht.** Die Werte stehen an mehreren Stellen von Hand
|
||||
abgeschrieben — in Swift für die Apps, in CSS für den Browser. Sie sind
|
||||
auseinandergelaufen, und zwar unsichtbar: `#1D1D21` gegen `#1D1D22`,
|
||||
`#A0A0A8` gegen `#B4B4BE`, Radius 8 gegen 6. Einzeln merkt das niemand.
|
||||
Zusammen sorgen sie dafür, dass App und Browser nicht wie dasselbe Programm
|
||||
aussehen.
|
||||
|
||||
**Warum kein Generator.** Einer müsste Swift erzeugen, und erzeugter Swift
|
||||
lässt sich schlechter lesen und kommentieren als geschriebener. Diese Prüfung
|
||||
kostet weniger und fängt dasselbe ab: sie sagt, dass etwas auseinanderläuft,
|
||||
und überlässt die Entscheidung dem Menschen.
|
||||
|
||||
Aufruf: python3 Tools/tokens-pruefen.py
|
||||
Rückgabe: 0, wenn alles übereinstimmt; sonst 1.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
WURZEL = Path(__file__).resolve().parent.parent
|
||||
SWIFT_FARBEN = WURZEL / "Packages/FlaroKit/Sources/FlaroDesign/Color/FlaroColor.swift"
|
||||
SWIFT_AKZENT = WURZEL / "Packages/FlaroKit/Sources/FlaroDesign/Color/AccentOption.swift"
|
||||
SWIFT_FORMEN = WURZEL / "Packages/FlaroKit/Sources/FlaroDesign/FlaroFont.swift"
|
||||
CSS = WURZEL / "web/app/src/style.css"
|
||||
|
||||
# Was womit zusammengehört. Links der Swift-Name, rechts das CSS-Token.
|
||||
FARBEN = {
|
||||
"canvas": "--canvas",
|
||||
"surface": "--surface",
|
||||
"elevated": "--elevated",
|
||||
"placeholder": "--placeholder",
|
||||
"textPrimary": "--text",
|
||||
"textSecondary": "--text-dim",
|
||||
"textMuted": "--text-muted",
|
||||
"destructive": "--destructive",
|
||||
}
|
||||
|
||||
# Radien: Swift-Name → CSS-Token. `sheet` und `panel` haben im Browser kein
|
||||
# Gegenstück — dort gibt es keine Blätter und keine schwebende Seitenleiste.
|
||||
RADIEN = {
|
||||
"card": "--r-karte",
|
||||
"control": "--r-flaeche",
|
||||
}
|
||||
|
||||
|
||||
def hex_aus_swift(text: str, name: str) -> str | None:
|
||||
"""Liest `static let name = RGB(hex: 0xAABBCC)` oder `RGB(0xAABBCC)`."""
|
||||
m = re.search(rf"\b{name}\b[^\n=]*=\s*[^\n]*?0x([0-9A-Fa-f]{{6}})", text)
|
||||
return "#" + m.group(1).upper() if m else None
|
||||
|
||||
|
||||
def ohne_tvos(text: str) -> str:
|
||||
"""Entfernt die tvOS-Zweige.
|
||||
|
||||
**Sonst vergleicht man den falschen Wert.** `FlaroRadius.card` ist 8 —
|
||||
und auf tvOS 12, weil aus drei Metern eine stärkere Rundung nötig ist.
|
||||
Der Browser läuft nie auf einem Fernseher; massgeblich ist der andere Zweig.
|
||||
"""
|
||||
return re.sub(r"#if os\(tvOS\).*?(?:#else(.*?))?#endif", lambda m: m.group(1) or "",
|
||||
text, flags=re.S)
|
||||
|
||||
|
||||
def zahl_aus_swift(text: str, name: str) -> float | None:
|
||||
"""Liest den ersten Zahlenwert hinter `static let name`."""
|
||||
m = re.search(rf"\b{name}\b\s*:\s*CGFloat\s*(?:\{{[^}}]*?return\s*)?([0-9]+(?:\.[0-9]+)?)", text, re.S)
|
||||
if m:
|
||||
return float(m.group(1))
|
||||
m = re.search(rf"\b{name}\b[^\n=]*=\s*([0-9]+(?:\.[0-9]+)?)", text)
|
||||
return float(m.group(1)) if m else None
|
||||
|
||||
|
||||
def token_aus_css(text: str, name: str) -> str | None:
|
||||
m = re.search(rf"{re.escape(name)}\s*:\s*([^;]+);", text)
|
||||
return m.group(1).strip() if m else None
|
||||
|
||||
|
||||
def main() -> int:
|
||||
fehlend = [p for p in (SWIFT_FARBEN, SWIFT_AKZENT, SWIFT_FORMEN, CSS) if not p.exists()]
|
||||
if fehlend:
|
||||
for p in fehlend:
|
||||
print(f"Nicht gefunden: {p.relative_to(WURZEL)}")
|
||||
return 1
|
||||
|
||||
farben_swift = SWIFT_FARBEN.read_text(encoding="utf-8")
|
||||
akzent_swift = SWIFT_AKZENT.read_text(encoding="utf-8")
|
||||
formen_swift = ohne_tvos(SWIFT_FORMEN.read_text(encoding="utf-8"))
|
||||
css = CSS.read_text(encoding="utf-8")
|
||||
|
||||
abweichungen: list[str] = []
|
||||
geprueft = 0
|
||||
|
||||
for swift_name, css_name in FARBEN.items():
|
||||
a = hex_aus_swift(farben_swift, swift_name)
|
||||
b = token_aus_css(css, css_name)
|
||||
if a is None or b is None:
|
||||
abweichungen.append(f" {swift_name} / {css_name}: nicht gefunden ({a} / {b})")
|
||||
continue
|
||||
geprueft += 1
|
||||
if a.upper() != b.upper():
|
||||
abweichungen.append(f" {swift_name}: Swift {a}, CSS {css_name} {b}")
|
||||
|
||||
# Die Vorgabefarbe der App gegen den Akzent im Browser.
|
||||
vorgabe = re.search(r'identifier\s*==\s*"([a-z]+)"', akzent_swift)
|
||||
if not vorgabe:
|
||||
vorgabe = re.search(r'\.first\s*\{\s*\$0\.id\s*==\s*"([a-z]+)"', akzent_swift)
|
||||
if vorgabe:
|
||||
hex_vorgabe = hex_aus_swift(akzent_swift, vorgabe.group(1))
|
||||
akzent_css = token_aus_css(css, "--accent")
|
||||
if hex_vorgabe and akzent_css:
|
||||
geprueft += 1
|
||||
if hex_vorgabe.upper() != akzent_css.upper():
|
||||
abweichungen.append(
|
||||
f" Vorgabe-Akzent: Swift {vorgabe.group(1)} {hex_vorgabe}, CSS {akzent_css}")
|
||||
|
||||
for swift_name, css_name in RADIEN.items():
|
||||
a = zahl_aus_swift(formen_swift, swift_name)
|
||||
b = token_aus_css(css, css_name)
|
||||
if a is None or b is None:
|
||||
abweichungen.append(f" Radius {swift_name} / {css_name}: nicht gefunden ({a} / {b})")
|
||||
continue
|
||||
geprueft += 1
|
||||
b_zahl = float(re.sub(r"[^0-9.]", "", b) or 0)
|
||||
if abs(a - b_zahl) > 0.01:
|
||||
abweichungen.append(f" Radius {swift_name}: Swift {a:g}, CSS {css_name} {b}")
|
||||
|
||||
if abweichungen:
|
||||
print(f"App und Weboberfläche laufen auseinander ({len(abweichungen)} Stelle(n)):")
|
||||
print("\n".join(abweichungen))
|
||||
print("\nFlaroDesign ist massgeblich — die CSS zieht nach.")
|
||||
return 1
|
||||
|
||||
print(f"App und Weboberfläche stimmen überein ({geprueft} Werte geprüft).")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -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
@@ -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; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user