diff --git a/public/diagrams/powierzchnia-ataku.svg b/public/diagrams/powierzchnia-ataku.svg
index 839e6f3..b887430 100644
--- a/public/diagrams/powierzchnia-ataku.svg
+++ b/public/diagrams/powierzchnia-ataku.svg
@@ -44,5 +44,5 @@
▪ git (Gitea) — tylko zalogowani
- docelowo za VPN, poza publicznym internetem
+ poza ścieżką czytelnika — awaria nie kładzie strony
diff --git a/src/content/news/architektura-demo-git-cms.md b/src/content/news/architektura-demo-git-cms.md
index 73c73b3..1b7517b 100644
--- a/src/content/news/architektura-demo-git-cms.md
+++ b/src/content/news/architektura-demo-git-cms.md
@@ -11,8 +11,11 @@ draft: false
Ten portal to **statyczne pliki HTML** generowane z plików Markdown trzymanych
w repozytorium git. Redaktor dostaje **graficzny edytor w przeglądarce** —
-a mimo to w produkcji nie działa żadna aplikacja, żadna baza danych.
-Ten artykuł został napisany i opublikowany dokładnie tą ścieżką.
+a mimo to **po stronie publicznej** nie działa żadna aplikacja ani baza:
+między czytelnikiem a treścią jest wyłącznie serwer plików. Jedyny działający
+serwis (Gitea) obsługuje edycję i stoi poza ścieżką czytelnika — wracamy do
+tego uczciwie niżej. Ten artykuł został napisany i opublikowany dokładnie tą
+ścieżką.
## Od zapisu do publikacji
@@ -62,14 +65,25 @@ i jednorazowy kontener builda odpalany przez systemd timer.
## Dwie klasy rozwiązań
-| | Statyczne + git-CMS *(to demo)* | CMS z własną bazą |
+Linia podziału jest często mylona: nie chodzi o to, czy treść leży w bazie
+czy w plikach, tylko o to, **czy między czytelnikiem a treścią stoi
+działająca aplikacja**.
+
+| | Statyczne + git-CMS *(to demo)* | CMS serwujący stronę aplikacją |
|---|---|---|
-| Przykłady | Sveltia, Decap (+ Astro / Hugo / Eleventy) | Strapi, WordPress, Grav, TinaCMS |
-| Treść żyje w | plikach Markdown w gicie | bazie danych |
-| W produkcji działa | serwer plików | aplikacja + baza, non stop |
-| Utrzymanie | skrypt builda | patche, aktualizacje, backupy bazy |
+| Przykłady | Sveltia, Decap (+ Astro / Hugo / Eleventy) | Strapi, WordPress, Drupal, Grav, TinaCMS |
+| Treść żyje w | plikach Markdown w gicie | zwykle w bazie; Grav — w plikach, Tina — w gicie |
+| W produkcji działa | serwer plików | aplikacja (zwykle + baza), non stop |
+| Utrzymanie | skrypt builda + serwis gitowy | patche, aktualizacje, backupy bazy |
| Historia zmian | git, wbudowana | zależnie od produktu (bywa płatna) |
+Dwa doprecyzowania, żeby tabela nie upraszczała: **Grav** trzyma treść
+w plikach bez żadnej bazy — ale stronę i tak serwuje aplikacja PHP, więc pod
+względem powierzchni ataku i utrzymania należy do prawej kolumny. **TinaCMS**
+trzyma treść wręcz w Markdownie w gicie, ale do działania edytora wymaga
+stale działającego backendu z bazą — jest hybrydą, która dziedziczy koszty
+obu światów.
+
Ograniczenia poszczególnych rozwiązań, które zweryfikowaliśmy podczas
researchu:
@@ -104,9 +118,31 @@ konkretnych produktów.
| | Statyczne + git | CMS z bazą |
|---|---|---|
-| Plusy | brak publicznie działającej aplikacji = brak presji łatek bezpieczeństwa; koszt utrzymania bliski zera; treść w otwartym formacie przetrwa każdą wymianę narzędzi | jeśli z czasem będą potrzebne funkcje aplikacyjne (konta, personalizacja, treści silnie strukturalne), platforma już je ma |
+| Plusy | strona publiczna bez aplikacji = brak presji łatek na ścieżce czytelnika; do utrzymania zostaje serwis gitowy (najlepiej ten, który zespół i tak ma); treść w otwartym formacie przetrwa każdą wymianę narzędzi | jeśli z czasem będą potrzebne funkcje aplikacyjne (konta, personalizacja, treści silnie strukturalne), platforma już je ma |
| Minusy | przy bardzo dużej skali (tysiące stron) buildy rosną i wymagają optymalizacji; zaawansowane funkcje redakcyjne zależą od tempa rozwoju narzędzi open-source | kilka dużych migracji wersji w 6 lat (zmiany łamiące); baza wymaga backupów i testów odtwarzania; stała powierzchnia ataku; ryzyko przenoszenia funkcji do płatnych planów |
+## „Czy Gitea to nie jest po prostu drugi CMS?"
+
+Uczciwe pytanie: Gitea też ma logowanie, bazę i wymaga aktualizacji
+bezpieczeństwa, a dla edytorów musi być dostępna spoza VPN. Czy różnica nie
+sprowadza się więc do tego, że zamiast CMS-a utrzymujemy Giteę?
+
+Częściowo tak — i dlatego nie twierdzimy, że utrzymanie jest zerowe. Różnica
+polega na trzech rzeczach:
+
+- **Gitea stoi poza ścieżką czytelnika.** Może mieć przerwę, aktualizację,
+ a nawet zostać przejęta — portal dalej stoi i serwuje bezpieczne pliki.
+ Najgorszy scenariusz włamania to wandalizm treści: widoczny, z pełną
+ historią, odwracalny jednym revertem. W CMS-ie serwującym stronę ten sam
+ incydent oznacza przejętą stronę publiczną.
+- **Zespół już utrzymuje Giteę.** Docelowo repozytorium treści może żyć na
+ istniejącej instancji zespołu — wtedy krańcowy koszt utrzymania jest
+ bliski zera, a logowanie załatwia jedna integracja z AAI.
+- **Klasa oprogramowania.** Gitea to pojedynczy program w Go z SQLite:
+ aktualizacja to podmiana obrazu i restart, bez ekosystemu pluginów i bez
+ drzewa zależności npm po stronie serwera. Platformy CMS to frameworki
+ aplikacyjne z regularnymi migracjami głównych wersji.
+
## Gdzie jeszcze może się przydać
Ten sam wzorzec — edytor graficzny nad repozytorium Markdown — nie jest
diff --git a/src/styles/global.css b/src/styles/global.css
index e08e0af..29b56e5 100644
--- a/src/styles/global.css
+++ b/src/styles/global.css
@@ -193,7 +193,9 @@ img { max-width: 100%; height: auto; display: block; }
/* Header — Galaxy Blue band, reverse (white) logo per BRAND B.1.2; translucent
over content where backdrop blur is supported (AA computed: worst underlying
white → bar (31,31,96), muted nav text 9.4:1). Solid Galaxy fallback. */
-.site-header { background: var(--galaxy); position: sticky; top: 0; z-index: 10; }
+/* Lifted shadow separates the bar (and the mobile dropdown panels
+ underneath it) from content — the header floats above the page. */
+.site-header { background: var(--galaxy); position: sticky; top: 0; z-index: 10; box-shadow: var(--shadow-lifted); }
@supports ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) {
.site-header {
background: color-mix(in srgb, var(--galaxy) 88%, transparent);
@@ -225,14 +227,15 @@ img { max-width: 100%; height: auto; display: block; }
}
/* Underline-grow (the current nav-hover convention): a 2px rule scales in
from the left on hover and stays put on the active page. Transform-only;
- its transition lives in the no-preference block like every other motion. */
-.nav a.navlink::after {
+ its transition lives in the no-preference block like every other motion.
+ ::before by codebase convention — ::after belongs to the external arrow. */
+.nav a.navlink::before {
content: ""; position: absolute; left: 10px; right: 10px; bottom: 4px;
height: 2px; border-radius: 1px; background: #fff;
transform: scaleX(0); transform-origin: 0 50%;
}
.nav a.navlink:hover { color: #fff; }
-.nav a.navlink:hover::after, .nav a.navlink[aria-current='page']::after { transform: scaleX(1); }
+.nav a.navlink:hover::before, .nav a.navlink[aria-current='page']::before { transform: scaleX(1); }
.nav a.navlink[aria-current='page'] { color: #fff; }
/* AAI sign-in entry — ghost/outline: Cherenkov fill is reserved for the page's
primary CTA, the header only whispers. */
@@ -802,7 +805,7 @@ mark { background: var(--tint-cherenkov); color: inherit; border-radius: calc(va
.card:hover, .search-results li:hover { transform: translateY(-2px); } /* the one elevate gesture (no cover zoom) */
/* Nav underline-grow + press feedback (micro-interaction pair) */
- .nav a.navlink::after { transition: transform var(--dur) var(--ease); }
+ .nav a.navlink::before { transition: transform var(--dur) var(--ease); }
.btn:active, .nav a.navlink:active { transform: translateY(1px); }
/* === Scroll-driven effects (CSS only, compositor-only, no JS) ===
@@ -811,17 +814,14 @@ mark { background: var(--tint-cherenkov); color: inherit; border-radius: calc(va
@supports (animation-timeline: view()) {
/* Cards drift in as they enter the viewport; items already on screen
start past the entry range, so nothing flashes at load. */
- .card { animation: card-in linear both; animation-timeline: view(); animation-range: entry 0% entry 35%; }
- /* Header casts its shadow only once the page is actually scrolled. */
- .site-header::before {
- content: ""; position: absolute; inset: 0; z-index: -1;
- box-shadow: var(--shadow-lifted); opacity: 0;
- animation: fade-in linear both; animation-timeline: scroll(); animation-range: 0 80px;
- }
+ /* Range tuned on a real trackpad: entry 0–35% completed within a flick
+ and read as "no effect"; 0–75% keeps the drift visible at normal
+ scrolling speed while still finishing before the card is fully in. */
+ .card { animation: card-in linear both; animation-timeline: view(); animation-range: entry 0% entry 75%; }
/* Article reading progress — functional state, same Cherenkov budget line
as the TOC scrollspy rule (navigation feedback, not decoration). */
.read-progress {
- position: fixed; top: 56px; left: 0; width: 100%; height: 2px; z-index: 9;
+ position: fixed; top: 56px; left: 0; width: 100%; height: 3px; z-index: 9;
background: var(--cherenkov); transform-origin: 0 50%;
animation: progress linear both; animation-timeline: scroll(root);
}
@@ -840,6 +840,6 @@ mark { background: var(--tint-cherenkov); color: inherit; border-radius: calc(va
to { transform: translate3d(5%, 5%, 0) rotate(10deg) scale(1.16); opacity: 1; }
}
@keyframes rise { from { opacity: 0; transform: translateY(14px); } }
-@keyframes card-in { from { opacity: 0; transform: translateY(14px); } }
+@keyframes card-in { from { opacity: 0; transform: translateY(28px); } }
@keyframes fade-in { from { opacity: 0; } }
@keyframes progress { from { transform: scaleX(0); } to { transform: scaleX(1); } }