/* Wydzielone 1:1 z docs/guc-net-pl-concept.html (Świadomie jeden, ciemny
   motyw — to terminal z 2004, nie ma trybu jasnego), pomniejszone o to, co
   było wyłącznie częścią mockupu-recenzenckiego (.reviewbar) albo panelu
   admina (logowanie/edytor — ten ma teraz własny, osobny CSS w
   templates/panel/_base.html). Ten plik jest WSPÓLNY dla lab/pawel/agata —
   patrz app/templates_engine.py — i kopiowany bez zmian do wszystkich
   trzech eksportów statycznych (Etap 2 buduje je w app/export/builder.py). */
:root{
  --void:#000000;
  --panel:#0a1a2c;
  --line:#2b3a4a;
  --text:#a9b7c6;
  --text-dim:#5f7085;
  --bright:#e6e9ec;
  --cyan:#6f93b3;
  --amber:#b98f52;
  --glitch-a:#a85d56;
  --glitch-b:#a8794f;
  --glitch-c:#a89a55;
  font-size:13px;
}
*{box-sizing:border-box;}
/* Reserves the vertical scrollbar's gutter PERMANENTLY, whether or not the
   page currently needs to scroll — otherwise a Mac with a mouse attached
   (classic, space-reserving scrollbar, not the trackpad overlay style)
   can show or hide it as PL/EN text settles at slightly different page
   heights, nudging anything centered with margin:auto sideways by the
   scrollbar's width. NOT what caused the pl/en jump reported in Safari —
   see the CRT-effect section below for that. Left in place because it is
   correct and cheap on its own for mouse users.

   `scrollbar-gutter` only takes effect on an element whose `overflow` is
   NOT the initial `visible` (confirmed 08.09.2026: with overflow left at
   its default, the declaration was silently a no-op and clientWidth kept
   flipping between the two values) — `overflow-y:scroll` forces `<html>`
   into that state, and on its own already reserves the space in every
   browser, `scrollbar-gutter` support or not. On macOS trackpad/overlay
   scrollbars (which never reserve layout space regardless) this is
   invisible; only classic, mouse-driven scrollbars ever show its track,
   which is exactly the one case that could otherwise jump. */
html{overflow-y:scroll;scrollbar-gutter:stable;}
body{
  background:var(--void);
  color:var(--text);
  font-family:'JetBrains Mono', ui-monospace, 'SFMono-Regular', Menlo, Consolas, monospace;
  line-height:1.3;
  margin:0;
}
h1{font-weight:700;color:var(--bright);font-size:16px;margin:0 0 10px;}
a{color:var(--cyan);}
button{font-family:inherit;}
::selection{background:var(--amber);color:#1a0f00;}

.void{padding:24px 12px 40px;}
.stage{max-width:800px;margin:0 auto;}

/* baner — na czarnej pustce, bez ramki, jak w oryginale */
.banner{
  display:grid;grid-template-columns:1fr auto;gap:18px;
  padding:0 6px 12px;
}
.banner-lines .line{margin:0 0 3px;font-size:12.5px;white-space:normal;word-break:break-word;}
.banner-lines .line:last-child{margin-bottom:0;}
.muted{color:var(--text-dim);}
.bright{color:var(--bright);font-weight:600;}
.cursor{display:inline-block;color:var(--amber);animation:blink 1s steps(1) infinite;}
@keyframes blink{50%{opacity:0;}}
.banner-side{text-align:right;min-width:170px;}
.wordmark{color:var(--cyan);font-weight:700;letter-spacing:.04em;}
.wordmark.glitch-a{color:var(--glitch-a);}
.wordmark.glitch-b{color:var(--glitch-b);}
.wordmark.glitch-c{color:var(--glitch-c);}
.wordmark{display:inline-block;transition:transform .04s linear;}
.easter-egg-link{color:inherit;text-decoration:none;}
.easter-egg-link:hover{color:inherit;text-decoration:underline;}
.easter-egg-link--error{color:var(--bright);}
.easter-egg-link--error:hover{color:var(--bright);}
.dotgrid{display:grid;grid-template-columns:repeat(3,6px);grid-template-rows:repeat(2,6px);gap:3px;justify-content:end;margin:5px 0 6px;}
.dotgrid span{width:6px;height:6px;border-radius:50%;background:var(--line);}
.comment-box p{margin:0 0 2px;font-size:11px;color:var(--text-dim);}

/* Menu + sidebar + stopka to JEDNA nieregularna powierzchnia w kształcie
   litery C — patrz layoutShapes() w common.js dla wymiarów. */
.lshape{position:relative;margin:12px 0 0;}
.lshape-fill{position:absolute;background:var(--panel);filter:blur(7px);pointer-events:none;}
.lshape-fill--menu{top:0;left:0;z-index:0;}
.lshape-fill--status{bottom:0;left:0;z-index:0;}
.lshape-fill--side{top:0;right:0;bottom:0;z-index:2;}
.lshape > *:not(.lshape-fill){position:relative;z-index:3;}

.navwrap{position:relative;padding:10px 16px;}
.navascii{margin:0;color:var(--cyan);font-size:12.5px;line-height:1.4;text-align:center;}
.navascii a{color:inherit;text-decoration:none;}
.navascii a:hover{color:var(--bright);}
.navascii .navactive{color:var(--amber);font-weight:700;}
.navascii .langlink{color:var(--text-dim);}
.navascii .langlink:hover{color:var(--bright);}
.nav-echo,.nav-close{position:absolute;color:var(--text-dim);font-size:12.5px;}
.nav-echo{top:10px;left:16px;}
.nav-close{bottom:10px;right:16px;}

.content{display:grid;grid-template-columns:1fr 190px;gap:22px;}
.content.single{grid-template-columns:1fr;}

/* Węższy layout — elementy znikają stopniowo. Progi i animacja "konsolowego"
   chowania/pokazywania siedzą w JS (BP_RULES/bpInit() w common.js) — tu
   zostaje tylko layoutowa KONSEKWENCJA zniknięcia sidebara. */
html.bp-nosidebar .content{grid-template-columns:1fr;}

.content__main{padding:12px 6px;}

.lede{font-size:13px;color:var(--text-dim);max-width:64ch;margin:0 0 14px;}
.block{margin:16px 0;}
.block-label{font-size:13px;color:var(--cyan);font-weight:700;margin:0 0 8px;}

.projitem{margin:0 0 14px;}
.projitem-head{font-size:13px;color:var(--bright);font-weight:700;margin:0 0 3px;}
.projitem p{margin:0 0 3px;font-size:13px;max-width:70ch;}
.projitem-links{font-size:12px;color:var(--text-dim);margin:0;}
.projitem-links a{color:var(--cyan);text-decoration:none;font-weight:700;}
.projitem-links a:hover{text-decoration:underline;}

.linksrow-plain{margin-top:16px;font-size:12px;color:var(--text-dim);}
.linksrow-plain a{color:var(--text-dim);text-decoration:none;}
.linksrow-plain a:hover{color:var(--cyan);}

/* sidebar — tło (pełna wysokość wiersza) daje osobny .lshape-fill--side;
   TA kolumna trzyma własną, naturalną wysokość treści (align-self:start,
   nie domyślny stretch), żeby nie zostawiać dużej pustej przestrzeni pod
   krótkim bio. Reveal/hide przy nawigacji lista<->wpis nie potrzebuje już
   własnej animacji na tym elemencie w ogóle — patrz .content niżej. */
.sidebar-plain{padding:12px 20px;font-size:12px;color:var(--text);align-self:start;}
.sidebar-plain p{margin:0 0 5px;}
.sidebar-label{color:var(--cyan);font-weight:700;margin:0 0 8px;font-size:13px;}
.sidebar-note{color:var(--text-dim);font-style:italic;margin-top:8px;}

/* stopka — jak stare `uptime`; tło daje wspólna .lshape-fill */
.statusbar{
  padding:8px 6px;font-size:11.5px;color:var(--text-dim);
  display:flex;justify-content:space-between;flex-wrap:wrap;gap:6px;
}
.client-load{white-space:nowrap;}
.signature{margin:6px 6px 0;text-align:right;font-size:11px;color:var(--text-dim);}

/* CMS chooses an intentional palette name, never arbitrary authored CSS. */
html[data-palette="phosphor"]{
  --panel:#0e2017;--line:#2d5140;--text:#b4cdbb;--text-dim:#698475;
  --bright:#edf8ef;--cyan:#8dbb97;--amber:#c6b86e;
  --glitch-a:#c27365;--glitch-b:#a99b61;--glitch-c:#75a17d;
}
html[data-palette="amber"]{
  --panel:#26190d;--line:#59452e;--text:#cfbea6;--text-dim:#89775d;
  --bright:#fff0d9;--cyan:#d6aa68;--amber:#e0bb72;
  --glitch-a:#c76e62;--glitch-b:#ca8a56;--glitch-c:#c1a052;
}
html[data-palette="magenta"]{
  --panel:#211325;--line:#51375a;--text:#cbb6cc;--text-dim:#826f88;
  --bright:#f4e8f3;--cyan:#bf8ebd;--amber:#d5a470;
  --glitch-a:#ce6f77;--glitch-b:#bc799c;--glitch-c:#af9b62;
}

/* wpisy — surowo: wyśrodkowany nagłówek data+tytuł, zwykły akapit */
.entries{margin-top:2px;}
.entry{margin-bottom:20px;}
.entry:last-child{margin-bottom:0;}
.entry-head{text-align:center;color:var(--cyan);font-weight:700;font-size:13.5px;margin:0 0 5px;}
.entry-meta{text-align:center;color:var(--text-dim);font-size:11px;margin:0 0 10px;}
.entry-body{margin:0 0 8px;font-size:13px;max-width:78ch;}
.entry-body:last-child{margin-bottom:0;}
.entry-body b,.entry-body strong{color:var(--bright);}
.entry-body a{color:var(--cyan);font-weight:700;text-decoration:none;}
.entry-body a:hover{text-decoration:underline;}
.entry-more{margin:6px 0 0;font-size:12px;}
.entry-more a{color:var(--text-dim);text-decoration:none;}
.entry-more a:hover{color:var(--cyan);}

/* wpis (widok pojedynczy) */
.postmeta-full{text-align:center;font-size:11.5px;color:var(--text-dim);margin:0 0 18px;}
.postbody p{max-width:74ch;margin:0 0 12px;}
.postbody{font-size:13px;}
.postbody b,.postbody strong{color:var(--bright);}
.postbody a{color:var(--cyan);font-weight:700;text-decoration:none;}
.postbody a:hover{text-decoration:underline;}
.postbody img{max-width:100%;height:auto;border:1px solid var(--line);cursor:zoom-in;}
.tagbar{margin:16px 0;font-size:12px;color:var(--text-dim);}

/* strona statyczna (o_mnie/kontakt) — treść jak wpis, bez metadanych wpisu */
.staticbody{font-size:13px;}
.staticbody p{max-width:74ch;margin:0 0 12px;}
.staticbody b,.staticbody strong{color:var(--bright);}
.staticbody a{color:var(--cyan);font-weight:700;text-decoration:none;}
.staticbody a:hover{text-decoration:underline;}
.staticbody img{max-width:100%;height:auto;border:1px solid var(--line);cursor:zoom-in;}

@media (prefers-reduced-motion: reduce){
  .cursor{animation:none;}
}

/* ======================================================================
   Konsolowa podmiana treści przy PRAWDZIWEJ nawigacji (lista wpisów <->
   pojedynczy wpis, w obrębie jednego bloga). ODRZUCONE: natywne
   Cross-Document View Transitions — nadanie [data-cx-content] własnej
   view-transition-name "wyciągało" ją z jednolitego zrzutu #stage, więc
   PRZY OKAZJI odpalała się też przy przełączniku pl/en (który WTEDY używał
   document.startViewTransition na całym #stage — patrz niżej, dziś już go
   nie używa) — dwie NIEZALEŻNE animacje na raz, stąd "ekran się zgniata,
   ale treść drukuje się osobno po konsolowemu". Poza tym natywny
   mechanizm nie umie wstrzyknąć żadnego elementu (żółtego kursora) w
   pseudo-element przejścia — tylko zrzut bitmapy z CSS-ową animacją.

   ODRZUCONE PO RAZ DRUGI (08.09.2026): pierwsza wersja tego efektu
   odsłaniała całe WIERSZE naraz (clip-path:inset() pionowy, @keyframes
   cx-eat-up/cx-cat, steps() = liczba wierszy), z kursorem skaczącym
   między nimi pionowo (cx-caret-up/cx-caret-down, translateY). Zgłoszone:
   powinien "rysować każdą literę kolejno, szybko, tak jak robi to ekran"
   — czyli iść POZIOMO przez tekst, nie skakać w pionie. @keyframes tu nie
   wystarczą: liczba kroków (znaków w wierszu) jest inna za każdym razem,
   a CSS wymaga stałej liczby w chwili pisania arkusza. Więc odsłanianie
   znak-po-znaku (cxTypewriter w common.js) jest w całości sterowane z
   JS przez requestAnimationFrame: co klatkę liczy nowy clip-path:polygon()
   na hoście i nową pozycję .cx-caret — bez CSS @keyframes/animation
   w ogóle po stronie odsłaniania. */

.cx-caret{
  position:absolute;
  background:var(--amber);
  pointer-events:none;
  z-index:4;
}

/* Ustawiane przez inline-skrypt w <head> (patrz templates/*/base.html)
   gdy wykryje ?enter=1 w URL — bez tego byłby błysk w pełni widocznej
   treści przed odegraniem wpisywania. cxTypewriter() w common.js zaczyna
   swój pierwszy renderowany stan od tego samego, w pełni zwiniętego
   wyglądu, więc przejście jest bez szwu. Ta sama klasa też ustawia
   wysokość .content — na WYSOKOŚĆ STAREJ STRONY (--cx-from-h, ustawione
   przez ten sam inline-skrypt z ?fromH=<px>), nie na zero — żeby kontener
   nie startował zwinięty (patrz obszerny komentarz nad
   playContentHeightEnter() w common.js: to właśnie ten zerowy start
   ukrywał żółty kursor cxTypewriter, obcinając go przez overflow:hidden
   rodzica). Brak ?fromH (np. wejście spoza tej animacji) daje 0px jak
   dawniej. */
html.cx-enter-pending [data-cx-content]{clip-path:inset(0 0 100% 0);}
html.cx-enter-pending .content{height:var(--cx-from-h, 0px);overflow:hidden;}

/* Poprzednie dwie próby (08.09.2026, dwie osobne poprawki) animowały
   boczny panel i jego tło NIEZALEŻNIE od treści głównej — osobnymi
   clip-path na .sidebar-plain i .lshape-fill--side, ze zgadywaniem, która
   krawędź ma być "przyklejona". Za każdym razem wychodziło źle: raz zła
   oś (pozioma zamiast pionowej), raz zły kierunek zwijania, i stopka
   (.statusbar/.lshape-fill--status) nigdy nie brała udziału w animacji,
   mimo że to ONA powinna być punktem odniesienia ("przyklejony do...
   paska... i razem z nim robić przestrzeń na treść" — użytkownik
   08.09.2026). Rozwiązanie okazało się prostsze niż cała ta choreografia:
   .content TO JEST "przestrzeń, w której jest treść" — jeśli TA rośnie/
   maleje naprawdę (height, nie clip-path), stopka idzie za nią zwykłym
   przepływem dokumentu, a .lshape-fill--side (position:absolute;
   top:0;bottom:0 względem .lshape) dostosowuje swoją wysokość SAM, bo
   .lshape jako całość też ma wtedy inną wysokość — bez JEDNEJ linijki
   kodu poświęconej bezpośrednio panelowi. Stąd JEDNA animacja
   (playContentHeight* w common.js) zamiast czterech.

   .content__main zachowuje WŁASNĄ choreografię znak-po-znaku
   (cxTypewriter, patrz sekcja "Konsolowa podmiana treści" wyżej) — jej
   pozycje wierszy są nadal liczone raz na starcie, ale teraz to bezpieczne: .content__main
   nie jest już ściskany przez zewnętrzną wysokość (align-items:stretch
   go nie dotyka, bo .content ma overflow:hidden, nie explicit height na
   dzieciach) — tylko WIDOCZNE OKNO (.content) rośnie/maleje wokół niego.
   Boczny tekst (.sidebar-plain) nie ma już WŁASNEJ animacji wcale —
   ujawnia się/znika jako NATURALNY skutek rosnącego/malejącego okna,
   dokładnie tak, jak podejrzewał użytkownik. */

/* Konsolowe chowanie/pokazywanie elementów na wąskich layoutach — kursor
   "zjada" tekst od prawej do lewej jak backspace; display:none pada
   DOPIERO po animacji (BP_RULES/bpInit() w common.js). */
@keyframes bp-eat  {from{clip-path:inset(0 0 0 0);}    to{clip-path:inset(0 100% 0 0);}}
@keyframes bp-type {from{clip-path:inset(0 100% 0 0);} to{clip-path:inset(0 0 0 0);}}
.bp-out {animation:bp-eat  180ms steps(16, start) both;}
.bp-in  {animation:bp-type 180ms steps(16, start) both;}
.bp-gone{display:none !important;}

@media (prefers-reduced-motion: reduce){
  .bp-out,.bp-in{animation:none;}
  .cx-caret{display:none;}
}

/* ======================================================================
   Efekt CRT — te same krzywe wył./wł. co w mockupie, ale odtwarzane
   RĘCZNIE: klasy CSS na ŻYWYM #stage, sterowane z common.js. Jeden
   mechanizm dla obu przypadków: przełącznika pl/en (ten sam dokument)
   i skoku lab<->paweł<->agata (RÓŻNE originy).

   ODRZUCONE: natywne View Transitions API (document.startViewTransition
   dla pl/en). Powód jest z tej samej rodziny co przy Cross-Document View
   Transitions wyżej, tylko kosztował trzy nieudane poprawki:

   Natywne przejście NIE animuje żywego elementu. Robi ZRZUT starego
   #stage, drugi zrzut nowego, wstawia oba do pseudo-drzewa
   ::view-transition (position:fixed w "snapshot containing block", czyli
   w OSOBNYM układzie współrzędnych) i interpoluje geometrię grupy.
   ::view-transition-group(app-view) dostaje przy tym własną, domyślną
   animację width/height/transform, której nadpisanie ::view-transition-old
   /new NIE dotyka (błąd poprzedniej poprawki: zamroziłem ::view-transition-
   -group(root), a to obejmuje tylko czarne tło POZA #stage — grupa niosąca
   samą widoczną stronę to app-view, nietknięta). A PL i EN mają realnie
   różny layout: #stage jest o ~17px niższy w PL, a kolumna `auto` w
   .banner (grid 1fr auto, sizowana max-content z .comment-box) skacze o
   ~66px — zmierzone na hubie przy oknie 960px (PL: "# laboratorium AI i
   rzeczy technicznych #" to dwa razy więcej znaków niż EN: "# an AI &
   tech lab #"). W Chromium tej różnicy nie widać, bo .banner-side ma
   text-align:right, ale zrzut nie wie nic o wyrównaniu: zapisuje
   współrzędne dosłownie. Na końcu przejścia nakładka znika i pojawia się
   żywy DOM — jeśli WebKit policzy origin nakładki choć odrobinę inaczej
   niż pozycję elementu (a liczy: w3c/csswg-drafts#11079 — pasek
   przewijania znika na czas przejścia; WebKit 78494 — przeliczanie
   overflow jest celowo odraczane w trakcie animacji), CAŁA strona
   przeskakuje w bok dokładnie w tej jednej klatce. Zgłoszone dwukrotnie w
   prawdziwym Safari (07 i 08.09.2026); w Chromium nie da się tego
   zreprodukować, więc nie da się tego zamknąć testem — to sam w sobie
   wystarczający powód, żeby nie budować na tym mechanizmie.

   Klasy niżej nie mają z tym nic wspólnego: animują ŻYWY element, tylko
   przez transform i opacity. Nie powstaje drugi układ współrzędnych, więc
   na końcu nie ma czego uzgadniać — i nie ma czego przeskoczyć. Cena:
   podmianę treści musi zgrać JS (setLang() w common.js). */
@keyframes tv-off{
  0%{opacity:1;transform:scaleY(1);filter:brightness(120%) contrast(150%);}
  40%{opacity:.4;transform:scaleY(.2);filter:brightness(200%) contrast(250%) saturate(300%);}
  60%{opacity:.2;transform:scaleY(.05);filter:brightness(300%) contrast(300%);}
  100%{opacity:0;transform:scaleY(0);}
}
@keyframes tv-on{
  0%{opacity:0;transform:scaleY(0);filter:brightness(400%) contrast(300%);}
  20%{opacity:.4;transform:scaleY(.1);filter:brightness(250%) contrast(200%);}
  40%{opacity:.8;transform:scaleY(1.1);filter:brightness(150%) contrast(180%);}
  60%{opacity:1;transform:scaleY(1);filter:brightness(120%) contrast(150%);}
  100%{opacity:1;transform:none;filter:brightness(100%) contrast(100%);}
}
#stage.cx-tv-out{animation:.35s ease-out both tv-off;}
#stage.cx-tv-in{animation:.35s ease-out both tv-on;}
/* Ustawiane przez inline-skrypt w <head> (patrz templates/*/base.html)
   ZANIM cokolwiek się wymaluje — bez tego byłby błysk w pełni widocznej
   strony, zanim common.js zdąży odpalić tv-on. */
html.cx-cross-pending #stage{opacity:0;}
@media (prefers-reduced-motion: reduce){
  #stage.cx-tv-out,#stage.cx-tv-in{animation:none;}
}

/* ======================================================================
   Przeglądarka zdjęć — obrazek w treści (.postbody/.staticbody, markdown
   ![]()) jest klikalny, otwiera się niemal na cały ekran z tym samym
   mrugnięciem monitora co reszta strony (te same @keyframes tv-off/tv-on
   wyżej, tylko na innym elemencie — cxLightboxInit() w common.js). */
.cx-lightbox{
  position:fixed;
  inset:0;
  z-index:1000;
  display:flex;
  align-items:center;
  justify-content:center;
  background:rgba(0,0,0,.92);
}
.cx-lightbox[hidden]{display:none;}
.cx-lightbox-stage{
  position:relative;
  width:100%;
  height:100%;
  display:flex;
  align-items:center;
  justify-content:center;
  overflow:hidden;
  touch-action:none;
}
.cx-lightbox-img{
  max-width:92vw;
  max-height:88vh;
  object-fit:contain;
  border:1px solid var(--line);
  cursor:zoom-in;
  -webkit-user-select:none;
  user-select:none;
  -webkit-user-drag:none;
}
.cx-lightbox-stage.is-zoomed .cx-lightbox-img{cursor:grab;}
.cx-lightbox-stage.is-zoomed .cx-lightbox-img:active{cursor:grabbing;}
.cx-lightbox-caption{
  position:absolute;
  left:0; right:0; bottom:16px;
  text-align:center;
  font-size:12px;
  color:var(--text-dim);
  pointer-events:none;
}
.cx-lightbox-close{
  position:absolute;
  top:16px; right:16px;
  width:38px; height:38px;
  border-radius:50%;
  border:1px solid var(--line);
  background:var(--panel);
  color:var(--bright);
  font-size:20px;
  line-height:1;
  cursor:pointer;
}
.cx-lightbox-close:hover{color:var(--amber);border-color:var(--amber);}
.cx-lightbox.cx-tv-out{animation:.35s ease-out both tv-off;}
.cx-lightbox.cx-tv-in{animation:.35s ease-out both tv-on;}
@media (prefers-reduced-motion: reduce){
  .cx-lightbox.cx-tv-out,.cx-lightbox.cx-tv-in{animation:none;}
}
