From acfc6fb6df274ad927bdb81909e0bf7b823b34d5 Mon Sep 17 00:00:00 2001 From: Jonas Weismueller Date: Mon, 27 Jul 2026 22:38:49 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20Website=20f=C3=BCr=20IONOS=20und=20neue?= =?UTF-8?q?=20Rubriken=20aktualisieren?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 + Dockerfile | 3 +- README.md | 37 + nginx.conf | 6 + server.mjs | 22 +- src/website/.htaccess | 60 ++ src/website/barrierefreiheit.html | 91 +++ .../website/datenschutz.html | 14 +- impressum.html => src/website/impressum.html | 14 +- index.html => src/website/index.html | 279 +++++-- .../public}/images/hardtschule-anbau.jpg | Bin .../public}/images/hardtschule-fassade.jpg | Bin .../public}/images/hardtschule-hero.jpg | Bin .../images/hardtschule-kinder-herz.webp | Bin .../website/public}/images/logo.png | Bin script.js => src/website/script.js | 36 +- styles.css => src/website/styles.css | 683 +++++++++++++++++- 17 files changed, 1153 insertions(+), 96 deletions(-) create mode 100644 .gitignore create mode 100644 src/website/.htaccess create mode 100644 src/website/barrierefreiheit.html rename datenschutz.html => src/website/datenschutz.html (79%) rename impressum.html => src/website/impressum.html (75%) rename index.html => src/website/index.html (66%) rename {public => src/website/public}/images/hardtschule-anbau.jpg (100%) rename {public => src/website/public}/images/hardtschule-fassade.jpg (100%) rename {public => src/website/public}/images/hardtschule-hero.jpg (100%) rename {public => src/website/public}/images/hardtschule-kinder-herz.webp (100%) rename {public => src/website/public}/images/logo.png (100%) rename script.js => src/website/script.js (90%) rename styles.css => src/website/styles.css (78%) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..52ed983 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.DS_Store +.playwright-cli/ +node_modules/ +npm-debug.log* diff --git a/Dockerfile b/Dockerfile index 8073f20..a25c8a6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,8 +5,7 @@ LABEL org.opencontainers.image.description="Statische Website der Hardtschule Ka LABEL org.opencontainers.image.source="https://git.servercentral.org/jonas/hardtschule-karlsruhe.de" COPY nginx.conf /etc/nginx/conf.d/default.conf -COPY index.html impressum.html datenschutz.html styles.css script.js /usr/share/nginx/html/ -COPY public/ /usr/share/nginx/html/public/ +COPY src/website/ /usr/share/nginx/html/ EXPOSE 80 diff --git a/README.md b/README.md index 0f9db77..23b548d 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,35 @@ Ein eigenständiger, responsiver Website-Prototyp auf Basis öffentlich auffindbarer Informationen der bisherigen Schulwebsite, des Webarchivs und der Stadt Karlsruhe. +## Öffentliches Website-Verzeichnis + +Alle Dateien, die auf einen Webspace gehören, liegen ausschließlich in: + +```text +src/website/ +├── .htaccess +├── index.html +├── impressum.html +├── datenschutz.html +├── barrierefreiheit.html +├── styles.css +├── script.js +└── public/ + └── images/ +``` + +Für einen Upload oder eine Synchronisation muss deshalb nur der **Inhalt von `src/website/`** in das Zielverzeichnis der Domain übertragen werden. Die versteckte Datei `.htaccess` muss dabei eingeschlossen sein. Dateien aus dem Projektstamm wie `Dockerfile`, `package.json`, `server.mjs` oder `.gitea/` gehören nicht auf den Webspace. + +## Auf IONOS/1&1 veröffentlichen + +1. Im IONOS-Konto unter **Domains & SSL** prüfen, mit welchem Webspace-Verzeichnis die Domain verbunden ist. +2. Das SSL-Zertifikat für `hardtschule-karlsruhe.de` aktivieren. +3. Per SFTP oder Webspace-Explorer den vollständigen Inhalt von `src/website/` in genau dieses Domain-Verzeichnis übertragen. +4. Darauf achten, dass `.htaccess` mit übertragen wurde; die empfohlenen Dateirechte sind `644`. +5. Anschließend `https://hardtschule-karlsruhe.de/`, die drei rechtlichen Unterseiten und ein Bild unter `public/images/` testen. + +Die `.htaccess` erzwingt HTTPS, deaktiviert Verzeichnislisten, aktiviert gzip-Komprimierung und Browser-Caches und setzt grundlegende Sicherheits-Header. Solange das SSL-Zertifikat noch nicht aktiv ist, muss der entsprechend kommentierte HTTPS-Block vorübergehend auskommentiert werden. + ## Lokal ansehen ```sh @@ -59,9 +88,17 @@ Der unveränderliche Commit-Tag erlaubt bei Bedarf ein gezieltes Rollback. `late - Stadt Karlsruhe: Städtischer Schülerhort an der Hardtschule - Werkraum Karlsruhe: Theaterprojekte 2023/24 +## Barrierefreiheit + +Die Seiten sind auf WCAG 2.2, Konformitätsstufe AA, ausgerichtet. Dazu gehören unter anderem semantische Seitenstrukturen, vollständige Tastaturbedienung, gut sichtbare Fokusmarkierungen, ausreichende Kontraste, Alternativtexte, große Bedienflächen, reduzierte Bewegung auf Wunsch sowie eine eigene Erklärung zur Barrierefreiheit mit Rückmeldemöglichkeit. + +Automatisierte Prüfungen ersetzen keine Tests mit assistiven Technologien und betroffenen Nutzer*innen. Nach größeren inhaltlichen oder gestalterischen Änderungen sollten deshalb erneut automatisierte und manuelle Tests durchgeführt werden. + ## Vor einer Veröffentlichung - aktuelle Termine, Materiallisten und Pläne ergänzen - Kontaktangaben und Zuständigkeiten von der Schule bestätigen lassen - Impressum und Datenschutz an Hosting und tatsächliche Dienste anpassen +- Erklärung zur Barrierefreiheit fachlich prüfen und mindestens jährlich aktualisieren +- Informationen in Deutscher Gebärdensprache und eine fachlich geprüfte Fassung in Leichter Sprache ergänzen - Nutzungsfreigabe für die Gebäudefotos dokumentieren (Dirk Altenkirch / Stadt Karlsruhe); das Hauptmotiv ist ein gekennzeichnetes KI-generiertes Symbolbild diff --git a/nginx.conf b/nginx.conf index 3a0dd51..1af180e 100644 --- a/nginx.conf +++ b/nginx.conf @@ -24,6 +24,12 @@ server { return 200 "ok\n"; } + location ~ /\.ht { + deny all; + access_log off; + log_not_found off; + } + location = / { expires -1; try_files /index.html =404; diff --git a/server.mjs b/server.mjs index 7d7a42f..45d2fe5 100644 --- a/server.mjs +++ b/server.mjs @@ -1,8 +1,8 @@ import { createReadStream, existsSync, statSync } from "node:fs"; import { createServer } from "node:http"; -import { extname, join, normalize } from "node:path"; +import { extname, join, relative, resolve, sep } from "node:path"; -const root = process.cwd(); +const root = resolve(process.cwd(), "src", "website"); const port = Number(process.env.PORT || 4173); const mimeTypes = { @@ -19,10 +19,16 @@ const mimeTypes = { createServer((request, response) => { const pathname = decodeURIComponent(new URL(request.url, `http://${request.headers.host}`).pathname); const requested = pathname === "/" ? "/index.html" : pathname; - const safePath = normalize(requested).replace(/^(\.\.(\/|\\|$))+/, ""); - let filePath = join(root, safePath); + let filePath = resolve(root, `.${requested}`); + const relativePath = relative(root, filePath); + const pathSegments = relativePath.split(sep); - if (!filePath.startsWith(root) || !existsSync(filePath)) { + if ( + relativePath === ".." || + relativePath.startsWith(`..${sep}`) || + pathSegments.some((segment) => segment.startsWith(".")) || + !existsSync(filePath) + ) { response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" }); response.end("Nicht gefunden"); return; @@ -32,6 +38,12 @@ createServer((request, response) => { response.writeHead(200, { "Content-Type": mimeTypes[extname(filePath).toLowerCase()] || "application/octet-stream", "Cache-Control": "no-cache", + "Content-Security-Policy": + "default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'; form-action 'self'", + "Permissions-Policy": "camera=(), microphone=(), geolocation=()", + "Referrer-Policy": "strict-origin-when-cross-origin", + "X-Content-Type-Options": "nosniff", + "X-Frame-Options": "SAMEORIGIN", }); createReadStream(filePath).pipe(response); }).listen(port, "127.0.0.1", () => { diff --git a/src/website/.htaccess b/src/website/.htaccess new file mode 100644 index 0000000..5fbf244 --- /dev/null +++ b/src/website/.htaccess @@ -0,0 +1,60 @@ +# Hardtschule Karlsruhe – Apache/IONOS Webhosting + +Options -Indexes -MultiViews +DirectoryIndex index.html +AddDefaultCharset UTF-8 + +# HTTPS erzwingen. Voraussetzung: Das SSL-Zertifikat ist im IONOS-Konto aktiv. + + RewriteEngine On + RewriteCond %{SERVER_PORT} !=443 + RewriteRule ^(.*)$ https://hardtschule-karlsruhe.de/$1 [R=301,L] + + +# Korrekte Auslieferung moderner Bildformate. + + AddType image/webp .webp + + +# Kompression für Textdateien. + + AddOutputFilterByType DEFLATE text/plain + AddOutputFilterByType DEFLATE text/html + AddOutputFilterByType DEFLATE text/css + AddOutputFilterByType DEFLATE application/javascript + AddOutputFilterByType DEFLATE application/json + AddOutputFilterByType DEFLATE application/xml + AddOutputFilterByType DEFLATE image/svg+xml + + +# Sinnvolle Browser-Caches: kurze Laufzeit für Code, lange für Bilder. + + ExpiresActive On + ExpiresByType text/html "access plus 0 seconds" + ExpiresByType text/css "access plus 1 hour" + ExpiresByType application/javascript "access plus 1 hour" + ExpiresByType image/jpeg "access plus 30 days" + ExpiresByType image/png "access plus 30 days" + ExpiresByType image/webp "access plus 30 days" + ExpiresByType image/svg+xml "access plus 30 days" + + + + Header always set X-Content-Type-Options "nosniff" + Header always set X-Frame-Options "SAMEORIGIN" + Header always set Referrer-Policy "strict-origin-when-cross-origin" + Header always set Permissions-Policy "camera=(), microphone=(), geolocation=()" + Header always set Content-Security-Policy "default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'; form-action 'self'" + + + Header set Cache-Control "no-cache, must-revalidate" + + + + Header set Cache-Control "public, max-age=3600" + + + + Header set Cache-Control "public, max-age=2592000" + + diff --git a/src/website/barrierefreiheit.html b/src/website/barrierefreiheit.html new file mode 100644 index 0000000..d6d816d --- /dev/null +++ b/src/website/barrierefreiheit.html @@ -0,0 +1,91 @@ + + + + + + + + Barrierefreiheit · Hardtschule Karlsruhe + + + + + + + +
+ + + +
+ + diff --git a/datenschutz.html b/src/website/datenschutz.html similarity index 79% rename from datenschutz.html rename to src/website/datenschutz.html index 407afaf..deb31c5 100644 --- a/datenschutz.html +++ b/src/website/datenschutz.html @@ -4,25 +4,27 @@ + Datenschutz · Hardtschule Karlsruhe + -
+
+
diff --git a/impressum.html b/src/website/impressum.html similarity index 75% rename from impressum.html rename to src/website/impressum.html index 13168cf..7fb6f2f 100644 --- a/impressum.html +++ b/src/website/impressum.html @@ -4,27 +4,29 @@ + Impressum · Hardtschule Karlsruhe + -
+
+
diff --git a/index.html b/src/website/index.html similarity index 66% rename from index.html rename to src/website/index.html index fadccde..1e3ebf0 100644 --- a/index.html +++ b/src/website/index.html @@ -64,6 +64,18 @@ + + + + + + + + + + + +
@@ -78,7 +90,7 @@ -
+
@@ -129,20 +148,20 @@ Eine lebendige Grundschule in Mühlburg, an der Kinder neugierig lernen, Verantwortung übernehmen und sich wohlfühlen können.

-
- Bewegte Grundschule - Soziales Lernen - Deutschförderung -
+
    +
  • Bewegte Grundschule
  • +
  • Soziales Lernen
  • +
  • Deutschförderung
  • +
-
+
+ +
+ +
-

Was uns ausmacht

+

Schulkonzept

Eine Schule, die Kinder ganzheitlich stärkt

Wissen, Bewegung, Sprache und ein respektvolles Miteinander gehören an der Hardtschule zusammen.

@@ -219,7 +288,7 @@
- 01 +

Lernen in Bewegung

Bewegungszeiten, bewegtes Lernen und abwechslungsreiche Pausen bringen Körper und Kopf in Schwung.

Seit 2010zertifizierte Grundschule mit sport- und bewegungserzieherischem Schwerpunkt
@@ -228,7 +297,7 @@
- 02 +

Stark miteinander

Klassenrat, Schüler*innenparlament und soziale Lernzeiten geben Kindern Stimme, Halt und Verantwortung.

Seit 2023verankert ein Sozialcurriculum die gemeinsame Präventionsarbeit im Schulalltag
@@ -237,7 +306,7 @@
- 03 +

Sprache öffnet Türen

In der Vorbereitungsklasse und der nachgehenden Förderung bekommen Kinder Zeit und Raum für Deutsch.

VKL & Förderungfür einen sicheren Einstieg in Unterricht, Freundschaften und den Schulalltag
@@ -273,11 +342,11 @@

Unser Ort

Ein Schulhaus mit Geschichte und Zukunft

Die Hardtschule prägt Mühlburg seit 1847. Hinter der historischen Fassade entsteht jeden Tag Neues: Ideen, Freundschaften und mutige erste Schritte.

-
-
1847erbaut in Mühlburg
-
2020denkmalgerecht saniert
-
direkt nebenanCarl-Benz-Halle für Sport & Feste
-
+
+
1847
erbaut in Mühlburg
+
2020
denkmalgerecht saniert
+
direkt nebenan
Carl-Benz-Halle für Sport & Feste
+

Zum Schulgelände gehören Klassenzimmer, Lesezimmer, Schülerhort, der Schulhof und die „Grüne Insel“ mit Spielgeräten.

+
+
+
+
+

Unsere Klassen

+

Gemeinsam durch die Grundschulzeit

+
+

Jede Klassenstufe hat ihre eigenen Entwicklungsschritte. Hier entsteht nach und nach ein lebendiger Einblick in die Lerngruppen.

+
+ +
+
+ + Klasse 1 +

Ankommen & entdecken

+

Vertraut werden, erste Buchstaben und Zahlen erforschen und als Klasse zusammenwachsen.

+
+
+ + Klasse 2 +

Sicherer werden

+

Grundlagen festigen, eigene Ideen entwickeln und immer selbstständiger arbeiten.

+
+
+ + Klasse 3 +

Neues erforschen

+

Zusammenhänge verstehen, Projekte gestalten und Verantwortung übernehmen.

+
+
+ + Klasse 4 +

Mutig weitergehen

+

Wissen vertiefen, Gemeinschaft stärken und den Übergang gut vorbereiten.

+
+
+ + Vorbereitungsklasse +

Deutsch lernen & dazugehören

+

Sprache, Orientierung und Begegnung öffnen Schritt für Schritt den Weg in den Schulalltag.

+ +
+
+
+
+
@@ -385,6 +500,79 @@
+
+
+
+

Kalender

+

Termine gut im Blick

+

Aktuelle Schultermine werden hier veröffentlicht, sobald sie von der Schule bestätigt sind.

+ + Ferien der Stadt Karlsruhe + + +
+ +
+ +
+ Wird laufend ergänzt +

Der Schulkalender wird vorbereitet

+

Bis zur Veröffentlichung erhalten Familien verbindliche Termine über die bekannten Informationswege der Schule.

+ + Termin anfragen + + +
+
+
+
+ +
+
+
+

Downloads

+

Wichtige Unterlagen schnell gefunden

+

Der Downloadbereich ist vorbereitet und kann mit den freigegebenen, aktuellen Dokumenten der Schule befüllt werden.

+
+ +
+
+ +
+ Wird ergänzt +

Materiallisten

+

Aktuelle Listen für die Klassen 1–4 und die Vorbereitungsklasse.

+
+ +
+ +
+ +
+ Wird ergänzt +

Formulare & Elternbriefe

+

Freigegebene Formulare und Informationen für Familien an einem Ort.

+
+ Unterlage anfragen +
+ +
+ +
+ Externer Kalender +

Ferien & freie Tage

+

Die veröffentlichten Schulferien für Karlsruhe direkt bei der Stadt nachsehen.

+
+ Ferienkalender öffnen +
+
+
+
+
@@ -419,7 +607,7 @@ 03Betreuung nach dem Unterricht

Der städtische Schülerhort befindet sich direkt an der Hardtschule. Verbindliche Angaben zu Angebot und Anmeldung stellt die Stadt Karlsruhe bereit.

- Zur Hort-Seite + Zur Hort-Seite
@@ -446,7 +634,7 @@

Kontakt & Anfahrt

Wir sind gern für Sie da.

Sie haben eine Frage? Rufen Sie an oder schreiben Sie uns. Bei einem Besuch melden Sie sich bitte zuerst im Sekretariat.

-
+

Hardtschule Karlsruhe
Hardtstraße 3
76185 Karlsruhe

@@ -461,12 +649,12 @@
-
+
-
+
@@ -489,21 +677,22 @@ - +
-

+

Informationen

diff --git a/public/images/hardtschule-anbau.jpg b/src/website/public/images/hardtschule-anbau.jpg similarity index 100% rename from public/images/hardtschule-anbau.jpg rename to src/website/public/images/hardtschule-anbau.jpg diff --git a/public/images/hardtschule-fassade.jpg b/src/website/public/images/hardtschule-fassade.jpg similarity index 100% rename from public/images/hardtschule-fassade.jpg rename to src/website/public/images/hardtschule-fassade.jpg diff --git a/public/images/hardtschule-hero.jpg b/src/website/public/images/hardtschule-hero.jpg similarity index 100% rename from public/images/hardtschule-hero.jpg rename to src/website/public/images/hardtschule-hero.jpg diff --git a/public/images/hardtschule-kinder-herz.webp b/src/website/public/images/hardtschule-kinder-herz.webp similarity index 100% rename from public/images/hardtschule-kinder-herz.webp rename to src/website/public/images/hardtschule-kinder-herz.webp diff --git a/public/images/logo.png b/src/website/public/images/logo.png similarity index 100% rename from public/images/logo.png rename to src/website/public/images/logo.png diff --git a/script.js b/src/website/script.js similarity index 90% rename from script.js rename to src/website/script.js index d1772c2..62e8055 100644 --- a/script.js +++ b/src/website/script.js @@ -1,5 +1,6 @@ const menuButton = document.querySelector("[data-menu-button]"); const mobileNav = document.querySelector("[data-mobile-nav]"); +const menuButtonLabel = menuButton?.querySelector(".sr-only"); const dialog = document.querySelector("[data-dialog]"); const dialogTitle = document.querySelector("[data-dialog-title]"); const dialogKicker = document.querySelector("[data-dialog-kicker]"); @@ -18,7 +19,7 @@ const detailContent = { `, actions: [ { label: "Sekretariat anrufen", href: "tel:+497211334686", primary: true }, - { label: "E-Mail schreiben", href: "mailto:schulleitung@hardtschule-karlsruhe.de" }, + { label: "E-Mail schreiben", href: "mailto:poststelle@hardtschule-ka.schule.bwl.de" }, ], }, materiallisten: { @@ -30,7 +31,7 @@ const detailContent = {
Gut erhaltene Schulsachen können in der Regel weiterverwendet werden – bitte immer die aktuelle Klassenliste beachten.
`, actions: [ - { label: "Liste anfragen", href: "mailto:schulleitung@hardtschule-karlsruhe.de?subject=Aktuelle%20Materialliste", primary: true }, + { label: "Liste anfragen", href: "mailto:poststelle@hardtschule-ka.schule.bwl.de?subject=Aktuelle%20Materialliste", primary: true }, { label: "Sekretariat anrufen", href: "tel:+497211334686" }, ], }, @@ -47,7 +48,7 @@ const detailContent = {

So können Kinder nach konzentrierten Arbeitsphasen neue Energie sammeln und Lernen mit allen Sinnen erleben.

`, - actions: [{ label: "Zur Schulgemeinschaft", href: "#gemeinschaft", primary: true }], + actions: [{ label: "Zum Schulleben", href: "#schulleben", primary: true }], }, miteinander: { kicker: "Soziales Lernen", @@ -73,7 +74,7 @@ const detailContent = {
Aufnahme und Förderumfang werden individuell mit Schule und zuständigen Stellen abgestimmt.
`, actions: [ - { label: "VKL-Anfrage senden", href: "mailto:schulleitung@hardtschule-karlsruhe.de?subject=Anfrage%20zur%20VKL", primary: true }, + { label: "VKL-Anfrage senden", href: "mailto:poststelle@hardtschule-ka.schule.bwl.de?subject=Anfrage%20zur%20VKL", primary: true }, { label: "Sekretariat anrufen", href: "tel:+497211334686" }, ], }, @@ -137,13 +138,14 @@ const detailContent = {

Mitgliedschaft und Mithilfe stärken ein solidarisches Miteinander im Interesse aller Kinder.

`, - actions: [{ label: "Mitgliedschaft anfragen", href: "mailto:schulleitung@hardtschule-karlsruhe.de?subject=Kontakt%20zum%20F%C3%B6rderverein", primary: true }], + actions: [{ label: "Mitgliedschaft anfragen", href: "mailto:poststelle@hardtschule-ka.schule.bwl.de?subject=Kontakt%20zum%20F%C3%B6rderverein", primary: true }], }, }; function setMenu(open) { if (!menuButton || !mobileNav) return; menuButton.setAttribute("aria-expanded", String(open)); + if (menuButtonLabel) menuButtonLabel.textContent = open ? "Menü schließen" : "Menü öffnen"; mobileNav.hidden = !open; document.body.classList.toggle("menu-open", open); } @@ -156,6 +158,23 @@ mobileNav?.querySelectorAll("a").forEach((link) => { link.addEventListener("click", () => setMenu(false)); }); +document.addEventListener("keydown", (event) => { + if (event.key !== "Escape") return; + + if (dialog?.open) { + dialog.close(); + return; + } + + if (menuButton?.getAttribute("aria-expanded") !== "true") return; + setMenu(false); + menuButton.focus(); +}); + +window.matchMedia("(min-width: 1051px)").addEventListener("change", (event) => { + if (event.matches) setMenu(false); +}); + function renderActions(actions = []) { dialogActions.innerHTML = ""; actions.forEach((action) => { @@ -171,10 +190,6 @@ function renderActions(actions = []) { }); } else { element.href = action.href; - if (action.external) { - element.target = "_blank"; - element.rel = "noreferrer"; - } if (action.href?.startsWith("#")) { element.addEventListener("click", () => dialog.close()); } @@ -201,6 +216,7 @@ function openDetail(key) { if (!dialog.open) dialog.showModal(); document.body.classList.add("dialog-open"); + requestAnimationFrame(() => dialogTitle.focus()); } document.querySelectorAll("[data-detail]").forEach((trigger) => { @@ -256,7 +272,7 @@ if ("IntersectionObserver" in window && navLinks.length) { navLinks.forEach((link) => { const active = link.getAttribute("href") === `#${visible.target.id}`; - if (active) link.setAttribute("aria-current", "true"); + if (active) link.setAttribute("aria-current", "location"); else link.removeAttribute("aria-current"); }); }, diff --git a/styles.css b/src/website/styles.css similarity index 78% rename from styles.css rename to src/website/styles.css index ca6536e..c9e9a08 100644 --- a/styles.css +++ b/src/website/styles.css @@ -5,7 +5,8 @@ --violet-700: #7655d8; --violet-100: #eee7ff; --mint-100: #dff6ec; - --coral-600: #ed6a61; + --coral-600: #c24b46; + --coral-700: #b94742; --coral-100: #ffe3de; --sun-500: #ffc857; --sun-100: #fff0bd; @@ -40,7 +41,7 @@ html { scroll-behavior: smooth; - scroll-padding-top: 108px; + scroll-padding-top: 96px; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } @@ -129,6 +130,10 @@ svg { padding: 112px 0; } +main[tabindex="-1"]:focus { + outline: none; +} + .svg-library, .sr-only { position: absolute; @@ -197,7 +202,7 @@ svg { min-height: 80px; display: flex; align-items: center; - gap: 30px; + gap: 20px; } .brand { @@ -263,7 +268,7 @@ svg { .desktop-nav { display: flex; align-items: center; - gap: 7px; + gap: 1px; margin-left: auto; } @@ -272,18 +277,19 @@ svg { display: grid; min-height: 44px; place-items: center; - padding: 0 12px; + padding: 0 8px; color: var(--ink-soft); - font-size: 0.9rem; + font-size: 0.78rem; font-weight: 650; + white-space: nowrap; } .desktop-nav a::after { content: ""; position: absolute; - right: 12px; + right: 8px; bottom: 6px; - left: 12px; + left: 8px; height: 2px; border-radius: 2px; background: var(--coral-600); @@ -295,7 +301,7 @@ svg { } .desktop-nav a:hover::after, -.desktop-nav a[aria-current="true"]::after { +.desktop-nav a[aria-current]::after { transform: scaleX(1); } @@ -431,6 +437,8 @@ svg { } .mobile-nav { + max-height: calc(100vh - 114px); + overflow-y: auto; border-top: 1px solid rgba(23, 48, 43, 0.08); background: var(--cream-50); } @@ -516,7 +524,7 @@ svg { } .eyebrow-light { - color: rgba(255, 255, 255, 0.78); + color: var(--white); } .eyebrow-light::before { @@ -549,12 +557,14 @@ svg { flex-wrap: wrap; gap: 12px 22px; margin-top: 44px; + padding: 0; color: rgba(255, 255, 255, 0.8); font-size: 0.75rem; font-weight: 650; + list-style: none; } -.hero-signals span { +.hero-signals li { display: inline-flex; align-items: center; gap: 8px; @@ -780,6 +790,130 @@ svg { .quick-green { background: var(--mint-100); } .quick-cream { background: var(--violet-100); } +.news-section { + background: + radial-gradient(circle at 92% 10%, rgba(118, 85, 216, 0.1), transparent 24%), + var(--cream-100); +} + +.section-heading-split.section-heading-dark > p { + color: var(--ink-soft); +} + +.news-grid { + display: grid; + grid-template-columns: 1.15fr 1fr 1fr; + gap: 16px; + margin-top: 52px; +} + +.news-card { + display: flex; + min-height: 400px; + flex-direction: column; + justify-content: space-between; + padding: 30px; + overflow: hidden; + border-radius: var(--radius-lg); + box-shadow: 0 0 0 1px rgba(37, 36, 81, 0.06), 0 14px 34px rgba(37, 36, 81, 0.08); +} + +.news-card-featured { + background: + radial-gradient(circle at 92% 8%, rgba(255, 200, 87, 0.24), transparent 32%), + linear-gradient(145deg, var(--blue-700), var(--violet-700)); + color: var(--white); +} + +.news-card-movement { + background: var(--sun-100); +} + +.news-card-classes { + background: var(--violet-100); +} + +.news-card-top { + position: relative; + z-index: 1; + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + color: var(--ink-soft); + font-size: 0.7rem; + font-weight: 750; +} + +.news-card-featured .news-card-top { + color: var(--white); +} + +.news-badge, +.download-status { + display: inline-flex; + width: fit-content; + min-height: 28px; + align-items: center; + padding: 4px 10px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.66); + color: var(--ink); + font-size: 0.65rem; + font-weight: 850; + letter-spacing: 0.06em; + text-transform: uppercase; +} + +.news-card-copy { + position: relative; + z-index: 1; +} + +.news-icon, +.download-icon { + display: grid; + width: 56px; + height: 56px; + place-items: center; + border-radius: 17px; + background: rgba(255, 255, 255, 0.68); + color: var(--blue-700); + box-shadow: 0 1px 4px rgba(37, 36, 81, 0.07); +} + +.news-card-featured .news-icon { + background: rgba(255, 255, 255, 0.14); + color: var(--sun-500); +} + +.news-icon svg, +.download-icon svg { + width: 28px; + height: 28px; +} + +.news-card h3 { + margin-top: 22px; + font-family: var(--font-display); + font-size: clamp(1.7rem, 2.4vw, 2.35rem); + font-weight: 400; +} + +.news-card p { + margin-top: 13px; + color: var(--ink-soft); + font-size: 0.86rem; +} + +.news-card-featured p { + color: var(--white); +} + +.news-card .text-link { + margin-top: 14px; +} + .section-heading h2 { font-size: clamp(2.55rem, 5vw, 4.35rem); } @@ -954,6 +1088,7 @@ svg { right: 0; bottom: 0; width: 44%; + height: auto; aspect-ratio: 1; object-fit: cover; border: 10px solid var(--cream-100); @@ -1008,14 +1143,15 @@ svg { align-items: center; } -.place-facts strong { +.place-facts dt { color: var(--green-900); font-family: var(--font-display); font-size: 1.17rem; font-weight: 400; } -.place-facts span { +.place-facts dd { + margin: 0; color: var(--ink-soft); font-size: 0.78rem; } @@ -1032,8 +1168,119 @@ svg { .image-source { display: block; margin-top: 16px; - color: rgba(23, 48, 43, 0.48); - font-size: 0.62rem; + color: var(--ink-soft); + font-size: 0.75rem; +} + +.classes-section { + background: + radial-gradient(circle at 5% 12%, rgba(255, 200, 87, 0.16), transparent 22%), + var(--cream-50); +} + +.classes-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 16px; + margin-top: 52px; +} + +.class-card { + position: relative; + display: flex; + min-height: 330px; + flex-direction: column; + overflow: hidden; + padding: 28px; + border-radius: var(--radius-lg); + box-shadow: 0 0 0 1px rgba(37, 36, 81, 0.06), 0 12px 30px rgba(37, 36, 81, 0.07); +} + +.class-card::after { + content: ""; + position: absolute; + width: 150px; + height: 150px; + top: -76px; + right: -58px; + border: 24px solid rgba(255, 255, 255, 0.35); + border-radius: 50%; +} + +.class-one { background: var(--sun-100); } +.class-two { background: var(--coral-100); } +.class-three { background: var(--blue-100); } +.class-four { background: var(--mint-100); } +.class-vkl { + min-height: 250px; + grid-column: span 4; + padding-right: min(42%, 470px); + background: + radial-gradient(circle at 86% 38%, rgba(255, 200, 87, 0.17), transparent 28%), + linear-gradient(145deg, var(--blue-700), var(--violet-700)); + color: var(--white); +} + +.class-number { + position: absolute; + top: 16px; + right: 22px; + color: rgba(37, 36, 81, 0.12); + font-family: var(--font-display); + font-size: 6.2rem; + line-height: 1; +} + +.class-number-vkl { + top: 30px; + right: 54px; + color: rgba(255, 255, 255, 0.1); + font-size: clamp(5rem, 12vw, 9rem); +} + +.class-label { + position: relative; + z-index: 1; + width: fit-content; + padding: 5px 10px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.68); + font-size: 0.65rem; + font-weight: 850; + letter-spacing: 0.06em; + text-transform: uppercase; +} + +.class-vkl .class-label { + background: rgba(255, 255, 255, 0.14); +} + +.class-card h3 { + position: relative; + z-index: 1; + margin-top: auto; + font-family: var(--font-display); + font-size: clamp(1.55rem, 2.2vw, 2rem); + font-weight: 400; +} + +.class-card p { + position: relative; + z-index: 1; + margin-top: 12px; + color: var(--ink-soft); + font-size: 0.82rem; +} + +.class-vkl p { + color: var(--white); +} + +.class-vkl .text-link { + position: relative; + z-index: 1; + margin-top: 12px; + color: var(--white); } .life-section { @@ -1499,13 +1746,19 @@ svg { } .parents-note span { - color: rgba(255, 255, 255, 0.65); - font-size: 0.66rem; + color: rgba(255, 255, 255, 0.9); + font-size: 0.75rem; } .parents-note a { + display: inline-flex; + min-height: 44px; + align-items: center; font-size: 1.05rem; font-weight: 800; + text-decoration: underline; + text-decoration-thickness: 0.08em; + text-underline-offset: 0.18em; } .info-accordion { @@ -1535,7 +1788,7 @@ svg { } .info-accordion summary > span { - color: rgba(255, 255, 255, 0.44); + color: rgba(255, 255, 255, 0.82); font-family: var(--font-sans); font-size: 0.68rem; font-weight: 800; @@ -1589,6 +1842,192 @@ svg { color: var(--white); } +.calendar-section { + background: + radial-gradient(circle at 0% 100%, rgba(237, 106, 97, 0.12), transparent 26%), + var(--cream-100); +} + +.calendar-grid { + display: grid; + grid-template-columns: 0.72fr 1.28fr; + align-items: center; + gap: clamp(54px, 8vw, 100px); +} + +.calendar-intro h2 { + margin-top: 18px; + font-size: clamp(2.8rem, 5vw, 4.5rem); +} + +.calendar-intro h2 em { + color: var(--coral-600); +} + +.calendar-intro > p:not(.eyebrow) { + margin-top: 22px; + color: var(--ink-soft); +} + +.calendar-intro .button { + margin-top: 28px; +} + +.calendar-card { + display: grid; + min-height: 410px; + grid-template-columns: 0.78fr 1.22fr; + gap: 10px; + padding: 10px; + border-radius: 34px; + background: var(--white); + box-shadow: 0 0 0 1px rgba(37, 36, 81, 0.06), 0 18px 45px rgba(37, 36, 81, 0.09); +} + +.calendar-sheet { + position: relative; + display: grid; + min-height: 100%; + place-content: center; + overflow: hidden; + border-radius: 24px; + background: + radial-gradient(circle at 84% 14%, rgba(255, 200, 87, 0.35), transparent 24%), + linear-gradient(145deg, var(--blue-700), var(--violet-700)); + color: var(--white); + text-align: center; +} + +.calendar-sheet > span { + position: absolute; + top: 0; + width: 16px; + height: 34px; + border-radius: 0 0 8px 8px; + background: var(--sun-500); +} + +.calendar-sheet > span:first-child { left: 28%; } +.calendar-sheet > span:nth-child(2) { right: 28%; } + +.calendar-sheet strong { + font-family: var(--font-display); + font-size: 2rem; + font-weight: 400; +} + +.calendar-sheet svg { + width: 72px; + height: 72px; + margin: 20px auto 0; + color: var(--sun-500); + stroke-width: 1.4; +} + +.calendar-empty { + display: flex; + align-items: flex-start; + justify-content: center; + flex-direction: column; + padding: 34px; +} + +.calendar-empty .news-badge { + background: var(--sun-100); +} + +.calendar-empty h3 { + margin-top: 20px; + font-family: var(--font-display); + font-size: clamp(1.8rem, 3vw, 2.5rem); + font-weight: 400; +} + +.calendar-empty p { + margin-top: 14px; + color: var(--ink-soft); + font-size: 0.86rem; +} + +.calendar-empty .text-link { + margin-top: 18px; +} + +.downloads-section { + background: var(--cream-50); +} + +.downloads-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 18px; + margin-top: 52px; +} + +.download-card { + display: flex; + min-height: 340px; + align-items: flex-start; + flex-direction: column; + padding: 30px; + border-radius: var(--radius-lg); + background: var(--white); + box-shadow: 0 0 0 1px rgba(37, 36, 81, 0.06), 0 12px 30px rgba(37, 36, 81, 0.06); + transition-property: box-shadow, transform; + transition-duration: 180ms; + transition-timing-function: ease-out; +} + +.download-card:hover { + transform: translateY(-3px); + box-shadow: 0 0 0 1px rgba(37, 36, 81, 0.08), 0 18px 38px rgba(37, 36, 81, 0.09); +} + +.download-icon { + background: var(--sun-100); + color: #7d5a05; +} + +.download-icon-coral { + background: var(--coral-100); + color: var(--coral-700); +} + +.download-icon-blue { + background: var(--blue-100); + color: var(--blue-700); +} + +.download-card > div { + margin-top: 24px; +} + +.download-status { + min-height: 26px; + padding-inline: 9px; + background: var(--cream-100); + color: var(--ink-soft); + font-size: 0.6rem; +} + +.download-card h3 { + margin-top: 13px; + font-family: var(--font-display); + font-size: 1.75rem; + font-weight: 400; +} + +.download-card p { + margin-top: 10px; + color: var(--ink-soft); + font-size: 0.84rem; +} + +.download-card .text-link { + margin-top: auto; + padding-top: 20px; +} + .contact-section { background: var(--cream-100); } @@ -1619,6 +2058,7 @@ svg { gap: 0; margin-top: 32px; border-top: 1px solid rgba(23, 48, 43, 0.14); + font-style: normal; } .contact-list > div { @@ -1659,11 +2099,20 @@ svg { color: var(--green-900); font-weight: 750; overflow-wrap: anywhere; + text-decoration: underline; + text-decoration-thickness: 0.08em; + text-underline-offset: 0.18em; +} + +.contact-list p a { + display: inline-flex; + min-height: 32px; + align-items: center; } .contact-list small { color: var(--ink-soft); - font-size: 0.68rem; + font-size: 0.75rem; } .map-card { @@ -1863,13 +2312,23 @@ svg { .footer-links a, .footer-links span { color: rgba(255, 255, 255, 0.68); - font-size: 0.78rem; + font-size: 0.82rem; transition-property: color; transition-duration: 160ms; } +.footer-links a { + display: inline-flex; + min-height: 32px; + align-items: center; + text-decoration: underline; + text-decoration-color: transparent; + text-underline-offset: 0.2em; +} + .footer-links a:hover { color: var(--white); + text-decoration-color: currentColor; } .footer-bottom { @@ -1878,15 +2337,31 @@ svg { align-items: center; justify-content: space-between; border-top: 1px solid rgba(255, 255, 255, 0.12); - color: rgba(255, 255, 255, 0.5); - font-size: 0.67rem; + color: rgba(255, 255, 255, 0.76); + font-size: 0.75rem; } .footer-bottom div { display: flex; + flex-wrap: wrap; gap: 22px; } +.footer-bottom a { + display: inline-flex; + min-height: 44px; + align-items: center; + text-decoration: underline; + text-decoration-color: transparent; + text-underline-offset: 0.2em; +} + +.footer-bottom a:hover, +.footer-bottom a:focus-visible { + color: var(--white); + text-decoration-color: currentColor; +} + .detail-dialog { width: min(660px, calc(100vw - 32px)); max-height: min(760px, calc(100vh - 32px)); @@ -1947,8 +2422,8 @@ svg { } .dialog-kicker { - color: var(--coral-600); - font-size: 0.68rem; + color: var(--coral-700); + font-size: 0.75rem; font-weight: 850; letter-spacing: 0.14em; text-transform: uppercase; @@ -2033,6 +2508,18 @@ svg { color: var(--ink-soft); } +.legal-content li + li { + margin-top: 8px; +} + +.legal-content strong { + color: var(--ink); +} + +.legal-intro { + font-size: 1.1rem; +} + .legal-content a { color: var(--green-900); font-weight: 700; @@ -2040,6 +2527,32 @@ svg { text-underline-offset: 3px; } +.legal-address { + margin-top: 18px; + color: var(--ink-soft); + font-style: normal; +} + +.legal-nav { + display: flex; + flex-wrap: wrap; + gap: 10px 28px; + max-width: 780px; + margin-top: 48px; + padding-top: 24px; + border-top: 1px solid rgba(23, 48, 43, 0.14); +} + +.legal-nav a { + display: inline-flex; + min-height: 44px; + align-items: center; + color: var(--green-900); + font-weight: 750; + text-decoration: underline; + text-underline-offset: 0.18em; +} + .legal-note { margin-top: 32px !important; padding: 20px; @@ -2066,8 +2579,9 @@ svg { } :focus-visible { - outline: 3px solid var(--sun-500); - outline-offset: 3px; + outline: 3px solid var(--white); + outline-offset: 2px; + box-shadow: 0 0 0 5px var(--navy-950); } @media (max-width: 1050px) { @@ -2096,6 +2610,23 @@ svg { grid-template-columns: repeat(3, 1fr); } + .news-grid { + grid-template-columns: 1fr 1fr; + } + + .news-card-featured { + grid-column: span 2; + min-height: 360px; + } + + .classes-grid { + grid-template-columns: 1fr 1fr; + } + + .class-vkl { + grid-column: span 2; + } + .profile-grid { grid-template-columns: 1fr 1fr; } @@ -2175,6 +2706,17 @@ svg { grid-template-columns: 1fr 1fr; } + .news-grid, + .downloads-grid { + grid-template-columns: 1fr; + } + + .news-card, + .news-card-featured { + min-height: 340px; + grid-column: auto; + } + .quick-card:last-child { grid-column: span 2; } @@ -2192,6 +2734,7 @@ svg { .place-grid, .parents-grid, + .calendar-grid, .contact-grid { grid-template-columns: 1fr; } @@ -2217,6 +2760,10 @@ svg { position: static; } + .calendar-card { + max-width: 620px; + } + .parents-intro > p:not(.eyebrow) { max-width: 560px; } @@ -2244,6 +2791,10 @@ svg { } @media (max-width: 600px) { + html { + scroll-padding-top: 84px; + } + :root { --shell: calc(100vw - 28px); } @@ -2350,13 +2901,41 @@ svg { transform: translate(3px, -50%); } + .news-card { + min-height: 330px; + padding: 25px; + border-radius: 24px; + } + .section-heading h2, .place-copy h2, .parents-intro h2, + .calendar-intro h2, .contact-copy h2 { font-size: 2.65rem; } + .classes-grid { + grid-template-columns: 1fr; + } + + .class-card, + .class-vkl { + min-height: 300px; + grid-column: auto; + padding: 25px; + } + + .class-vkl { + padding-right: 25px; + } + + .class-number-vkl { + top: 38px; + right: 20px; + font-size: 5rem; + } + .profile-card, .profile-card:last-child { min-height: 460px; @@ -2438,6 +3017,23 @@ svg { padding-left: 44px; } + .calendar-card { + grid-template-columns: 1fr; + } + + .calendar-sheet { + min-height: 230px; + } + + .calendar-empty { + padding: 26px 18px 28px; + } + + .download-card { + min-height: 310px; + padding: 26px; + } + .map-illustration { min-height: 390px; } @@ -2499,4 +3095,39 @@ svg { transform: none; filter: none; } + + .button:active, + .quick-card:active, + .dialog-close:active, + .menu-button:active { + scale: 1; + } +} + +@media (forced-colors: active) { + :focus-visible { + outline: 3px solid CanvasText; + box-shadow: none; + } + + .brand-mark, + .quick-card, + .news-card, + .profile-card, + .class-card, + .story-card, + .community-card, + .calendar-card, + .download-card, + .map-card, + .parents-note, + .dialog-note { + border: 1px solid CanvasText; + } + + .signal-dot, + .eyebrow::before, + .desktop-nav a::after { + background: CanvasText; + } }