/* ==========================================================================
   Sekcje strony
   ========================================================================== */

/* --- 00 · hero: widok z kabiny ------------------------------------------- */

.hero {
  position: relative;
  min-height: 100svh;
  display: grid;
  /* Kolumna musi być twardo związana z szerokością kadru - taśma odczytów
     jest szersza od ekranu i bez tego rozpycha całą siatkę. */
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: 1fr auto;
  overflow: hidden;
  background: var(--ink);
}

#hero-field {
  position: absolute;
  inset: 0;
  z-index: var(--z-scene);
  width: 100%;
  height: 100%;
}

/* winieta domykająca kadr - bez niej tor „rozlewa się" na krawędziach */
.hero::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background: radial-gradient(
      120% 80% at 50% 55%,
      transparent 30%,
      rgba(8, 7, 26, 0.82) 100%
    ),
    linear-gradient(to bottom, rgba(8, 7, 26, 0.6) 0%, transparent 22%);
}

.hero-body {
  position: relative;
  z-index: var(--z-content);
  align-self: center;
  width: min(100% - var(--gutter) * 2, var(--shell));
  margin-inline: auto;
  padding-left: var(--spine-w);
  padding-block: clamp(4rem, 9vh, 7rem) 2.25rem;
}

/* Logo idzie na prawą stronę kadru, przeciwwagą dla nagłówka i tekstu, które
   trzymają całą lewą połowę. Wyjęte z przepływu, żeby h1 zaczynał się u góry
   kadru, a nie pod logotypem. */
.hero-plate {
  position: absolute;
  top: clamp(4rem, 9vh, 7rem);
  right: 0;
}

/* Stałe 200 px: to minimalna szerokość znaku, jaką dopuszcza identyfikacja
   wizualna. Sufit z wysokości kadru byłby tu zbędny - znak jest płaski
   (2,2:1) i przy 200 px ma raptem 90 px wysokości. */
.hero-plate img {
  width: 12.5rem;
  height: auto;
}

/* Poniżej tej szerokości nagłówek dochodzi do prawej krawędzi kadru i logo
   położyłoby się na literach. Wraca wtedy nad tekst, do przepływu. */
@media (max-width: 64rem) {
  .hero-plate {
    position: static;
    display: inline-block;
    margin-bottom: clamp(1.75rem, 4vh, 2.75rem);
  }
}

/* Nagłówek skalowany szerokością kadru, ale z sufitem z jego wysokości.
   Bez tego na niskim laptopie (744 px) hero przelewał się o 55 px i taśma
   ze standardami - czyli najmocniejszy sygnał wiarygodności na tym ekranie -
   lądowała pod zgięciem. */
.hero h1 {
  font-size: min(var(--step-display), 14.5vh);
  font-weight: 800;
  line-height: 0.92;
  text-transform: uppercase;
}

.hero h1 .line {
  display: block;
  overflow: hidden;
  padding-bottom: 0.06em;
}

.hero h1 .line > span {
  display: block;
}

.js-motion .hero h1 .line > span {
  transform: translate3d(0, 105%, 0);
}

/* Pomarańczowy obrys na drugim członie nazwy. Wariant biały sprawdzony
   i odrzucony: z logo po prawej stronie kadru hero robił się jednolicie
   biały, a pomarańcz zostawał tylko na przycisku. */
.hero h1 .line--lit > span {
  color: transparent;
  -webkit-text-stroke: 1.5px var(--signal);
}

/* Bez obsługi obrysu tekst zostałby przezroczysty, czyli niewidoczny. */
@supports not (-webkit-text-stroke: 1px white) {
  .hero h1 .line--lit > span {
    color: var(--signal);
  }
}

.hero-sub {
  margin-top: clamp(1.4rem, 3vh, 2rem);
  max-width: 30rem;
  color: var(--steel);
  font-size: clamp(1rem, 0.5vw + 0.9rem, 1.2rem);
  line-height: 1.55;
}

.hero-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  margin-top: clamp(1.75rem, 4vh, 2.5rem);
}

.hero-foot {
  position: relative;
  z-index: var(--z-content);
  background: linear-gradient(to top, rgba(8, 7, 26, 0.92), transparent);
}

.hero-scroll {
  position: absolute;
  right: clamp(1rem, 3vw, 2.5rem);
  bottom: 5.5rem;
  z-index: var(--z-content);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.85rem;
  font-family: var(--font-mono);
  font-size: 0.625rem;
  letter-spacing: var(--track-mono);
  text-transform: uppercase;
  color: var(--steel-dim);
  writing-mode: vertical-rl;
}

.hero-scroll .bar {
  width: 1px;
  height: 3.5rem;
  background: linear-gradient(to bottom, var(--steel-dim), transparent);
  overflow: hidden;
}

.hero-scroll .bar::after {
  content: "";
  display: block;
  width: 100%;
  height: 40%;
  background: var(--signal);
  animation: scroll-run 2.2s var(--ease-inout) infinite;
}

@keyframes scroll-run {
  0% {
    transform: translateY(-100%);
  }
  60%,
  100% {
    transform: translateY(350%);
  }
}

@media (max-width: 60rem) {
  .hero-scroll {
    display: none;
  }
}

/* Na monitorze kolumna tekstu mieści się w lewej, ciemnej części kadru, a tor
   świeci obok niej. Na telefonie ta sama kolumna rozciąga się na całą szerokość
   i akapit ląduje wprost na rozjaśnionych szynach, w dodatku ruchomych. Winieta
   z .hero::after tu nie pomaga, bo jest przezroczysta dokładnie na środku.

   Stąd lokalna przesłona pod samym blokiem treści, wypuszczona poza margines na
   pełną szerokość okna, żeby nie było widać jej krawędzi. Przyciemnianie całego
   kadru rozwiązałoby to samo, ale kosztem animacji toru, czyli jedynej rzeczy,
   która w hero pokazuje, że to strona o pojazdach w ruchu. */
@media (max-width: 48rem) {
  .hero-body::before {
    content: "";
    position: absolute;
    z-index: -1;
    /* Stopnie w jednostkach bezwzględnych, nie w procentach. Treść dochodzi
       do samej dolnej krawędzi .hero-body, więc wygaszanie liczone procentowo
       wypadało na przyciskach: sprawdzone, krycie spadało tam z 0.65 do zera
       dokładnie na „Cztery przepisy". Przy calc(100% - 4rem) pełne krycie
       kończy się tam, gdzie kończy się tekst, a zejście do zera dzieje się
       już pod nim, niezależnie od wysokości ekranu. */
    inset: 0 calc(var(--gutter) * -1) -4rem;
    pointer-events: none;
    background: linear-gradient(
      to bottom,
      transparent 0,
      rgba(8, 7, 26, 0.88) 5rem,
      rgba(8, 7, 26, 0.88) calc(100% - 4rem),
      transparent 100%
    );
  }

  .hero-sub {
    color: #b6bcdb;
  }

  /* 11.5 px wersalikami z rozstrzeleniem 0.18em czyta się na monitorze
     z odległości 60 cm. Na telefonie trzymanym w ruchu już nie. */
  .hero-actions .btn {
    font-size: 0.8rem;
    letter-spacing: 0.12em;
  }

  /* Obwódka o kryciu 0.18 ginie na tle toru i przycisk przestaje wyglądać
     na klikalny. Zostaje sam napis wiszący w powietrzu. */
  .hero-actions .btn--ghost {
    border-color: rgba(154, 162, 201, 0.5);
    color: #c3c9e4;
  }
}

/* --- 01 · rozwiązanie ----------------------------------------------------- */

/* Siatka rysuje własne linie tłem prześwitującym przez odstęp 1 px, więc
   liczba kolumn musi dzielić liczbę kart bez reszty - inaczej ostatni rząd
   zostawia puste szare pola. Stąd sztywne 3 albo 1, bez auto-fill. */
.solution-grid,
.caps-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 1px;
  background: var(--hair-light);
  border: 1px solid var(--hair-light);
  border-radius: 3px;
  overflow: hidden;
}

.solution-grid {
  margin-top: clamp(2.5rem, 5vh, 3.5rem);
}

.solution-grid .card,
.caps-grid .card {
  border: none;
  border-radius: 0;
  background: var(--paper-raise);
}

@media (min-width: 60rem) {
  .solution-grid,
  .caps-grid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}

/* --- 02 · przepisy: karty wjeżdżające jedna pod drugą -------------------- */

.regs {
  background: var(--ink);
  position: relative;
}

/* delikatna siatka planu sieci w tle */
.regs::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0.5;
  background-image: linear-gradient(var(--hair-dark) 1px, transparent 1px),
    linear-gradient(90deg, var(--hair-dark) 1px, transparent 1px);
  background-size: 5.5rem 5.5rem;
  -webkit-mask-image: radial-gradient(90% 60% at 50% 40%, #000, transparent);
  mask-image: radial-gradient(90% 60% at 50% 40%, #000, transparent);
}

.regs > * {
  position: relative;
}

/* --- oferta i współpraca R&D ---------------------------------------------
   Cztery usługi w siatce 2 × 2 na ciemnym tle, a niżej dwa bloki współpracy
   w tej samej siatce. Karty nie są rozróżniane kolorem: paleta ma granat,
   pomarańcz i biel, a pomarańcz jest zarezerwowany dla stanów żywych (patrz
   tokens.css). Rozróżnienie niesie numer i tytuł, nie barwa.

   Obie sekcje dzielą wygląd, ale nie klasy: generator dokumentu do redakcji
   rozpoznaje sekcje po klasie siatki i przy wspólnej nazwie wpisałby
   współpracę do dokumentu jako kolejną usługę.
   ------------------------------------------------------------------------ */

.offer-grid,
.rnd-grid {
  margin-top: clamp(2.5rem, 5vh, 3.5rem);
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 1px;
  background: var(--hair-dark);
  border: 1px solid var(--hair-dark);
  border-radius: 3px;
  overflow: hidden;
}

.offer,
.rnd {
  position: relative;
  padding: clamp(1.4rem, 2.4vw, 2rem);
  background: var(--ink-raise);
}

/* Ta sama smuga co na kartach jasnych sekcji - spójność zachowania, nie kopia
   wyglądu. */
.offer::before,
.rnd::before {
  content: "";
  position: absolute;
  inset: auto 0 0 0;
  height: 2px;
  background: linear-gradient(90deg, var(--signal), transparent 70%);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--dur-slow) var(--ease-out);
}

.offer:hover::before,
.rnd:hover::before {
  transform: scaleX(1);
}

.offer-index,
.rnd-index {
  display: block;
  margin-bottom: 1rem;
  font-family: var(--font-mono);
  font-size: var(--step-mono);
  letter-spacing: var(--track-mono);
  color: var(--signal);
}

/* Flex z zawijaniem zamiast marginesu przy dopisku: gdy „powered by AI" nie
   mieści się obok nazwy, schodzi do własnej linii i staje równo z tytułem.
   Margines lewy zostawiał wtedy wiszące wcięcie 10 px. */
.offer h3 {
  margin-bottom: 0.9rem;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem 0.6rem;
  color: var(--white);
}

/* „powered by AI" jest dopiskiem producenta, nie częścią nazwy produktu -
   stąd osobny, mniejszy stopień. */
.offer-tag {
  display: inline-block;
  padding: 0.15rem 0.5rem;
  border: 1px solid var(--hair-dark);
  border-radius: 2px;
  vertical-align: middle;
  font-family: var(--font-mono);
  font-size: var(--step-mono);
  letter-spacing: var(--track-mono);
  text-transform: uppercase;
  color: var(--steel);
}

.offer ul {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
}

.offer li {
  position: relative;
  padding-left: 1.1rem;
  color: var(--steel-bright);
}

.offer li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.62em;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--signal-dim);
}

.rnd h3 {
  margin-bottom: 0.9rem;
  color: var(--white);
}

.rnd p {
  color: var(--steel-bright);
}

/* Jedyne odwołanie wychodzące poza ten serwis, stąd własna klasa zamiast stylu
   na `a` w sekcji: gdyby doszły kolejne linki, mają wyglądać tak samo z wyboru,
   nie przez przypadek. Podkreślenie pojawia się dopiero pod kursorem, bo stały
   akcent pomarańczowy w karcie należy do numeru porządkowego.

   `inline-block`, a nie flex: w karcie węższej niż ~480 px podpis nie mieści się
   w jednej linii, a flex zsyłał wtedy samą strzałkę do drugiego wiersza. Tutaj
   napis zawija się jak zwykły tekst, strzałkę trzyma przy ostatnim słowie twarda
   spacja w HTML, a podkreślenie na `text-decoration` idzie pod obie linie -
   `border-bottom` podkreślałby tylko dolną krawędź całego bloku. */
.rnd-link {
  display: inline-block;
  margin-top: 1.2rem;
  font-family: var(--font-mono);
  font-size: var(--step-mono);
  letter-spacing: var(--track-mono);
  line-height: 1.6;
  color: var(--signal);
  text-underline-offset: 0.35em;
}

.rnd-link .arrow {
  display: inline-block;
  margin-left: 0.15rem;
  transition: transform var(--dur-mid) var(--ease-out);
}

.rnd-link:hover {
  text-decoration: underline;
}

.rnd-link:hover .arrow {
  transform: translateX(0.3rem);
}

@media (min-width: 48rem) {
  .offer-grid,
  .rnd-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

/* --- strona 404 ----------------------------------------------------------
   Osobny układ, bo 404.html nie ładuje JS ani osi bocznej: bez zerowania
   padding-left ze .shell treść stałaby w miejscu zarezerwowanym dla toru,
   którego na tej stronie nie ma.
   ------------------------------------------------------------------------ */

.notfound {
  min-height: 100vh;
  padding-left: 0;
  padding-block: clamp(3rem, 10vh, 6rem);
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
  gap: 0.75rem;
}

.notfound img {
  height: 2.6rem;
  width: auto;
  margin-bottom: clamp(1.5rem, 5vh, 3rem);
}

.notfound h1 {
  margin-top: 0.25rem;
  font-family: var(--font-display);
  font-variation-settings: "wdth" var(--wdth-narrow);
  font-weight: 900;
  font-size: clamp(2.5rem, 8vw, 5rem);
  line-height: 1;
  letter-spacing: -0.02em;
  text-transform: uppercase;
}

.notfound .lead {
  max-width: 46ch;
}

/* Wersja angielska jest tu dopiskiem, nie równorzędnym akapitem: ktoś, kto
   trafił pod zły adres, ma najpierw zobaczyć komunikat w języku strony. */
.notfound .lead[lang="en"] {
  color: var(--steel-dim);
  font-size: var(--step-small);
}

.notfound .hero-actions {
  margin-top: clamp(1.5rem, 5vh, 2.5rem);
}

.reg-stack {
  margin-top: clamp(2rem, 5vh, 3rem);
}

/* Każda kolejna karta zatrzymuje się 0.9rem niżej niż poprzednia, dzięki czemu
   spod wjeżdżającej widać krawędź tej pod spodem i stos czyta się jako talia,
   a nie jako jedna karta.

   Wartości siedzą tutaj, a nie w atrybutach style= w HTML, żeby CSP nie
   musiało dopuszczać 'unsafe-inline' dla stylów. Przy okazji zmiana rozstawu
   to teraz jedna liczba w jednym miejscu zamiast czterech w znaczniku. */
.reg:nth-of-type(1) { --reg-offset: 0rem; }
.reg:nth-of-type(2) { --reg-offset: 0.9rem; }
.reg:nth-of-type(3) { --reg-offset: 1.8rem; }
.reg:nth-of-type(4) { --reg-offset: 2.7rem; }

.reg {
  position: sticky;
  top: calc(var(--nav-h) + 1.5rem + var(--reg-offset, 0rem));
  margin-bottom: clamp(1.5rem, 6vh, 4rem);
  padding: clamp(1.75rem, 3.5vw, 3rem);
  border: 1px solid var(--hair-dark);
  border-radius: 4px;
  /* Tło musi być kryjące - karty nachodzą na siebie i przezroczystość
     zmieniłaby stos w nieczytelną kaszę. */
  background: linear-gradient(150deg, #14123a 0%, #241f60 100%);
  box-shadow: 0 -1px 0 rgba(255, 255, 255, 0.05) inset,
    0 40px 90px -50px rgba(0, 0, 0, 0.95);
  transform-origin: 50% 0%;
  will-change: transform;
}

/* Cień kładziony przez kartę wjeżdżającą powyżej - sterowany z JS. */
.reg-veil {
  position: absolute;
  inset: 0;
  border-radius: 4px;
  background: var(--ink);
  opacity: 0;
  pointer-events: none;
}

/* pasek sygnałowy u góry karty */
.reg::before {
  content: "";
  position: absolute;
  inset: -1px -1px auto -1px;
  height: 2px;
  border-radius: 4px 4px 0 0;
  background: linear-gradient(90deg, var(--signal), rgba(241, 101, 33, 0) 55%);
}

.reg-head {
  display: flex;
  align-items: baseline;
  gap: clamp(1rem, 3vw, 2.5rem);
  padding-bottom: 1.4rem;
  margin-bottom: 1.4rem;
  border-bottom: 1px solid var(--hair-dark);
}

.reg-num {
  font-family: var(--font-mono);
  font-size: var(--step-mono);
  letter-spacing: var(--track-mono);
  color: var(--signal);
  flex-shrink: 0;
  padding-top: 0.35rem;
}

/* min-width: 0 jest tu konieczne, nie kosmetyczne. .reg-head to flex, a element
   flex ma domyślnie min-width: auto i nie zwęża się poniżej najdłuższego
   nierozdzielnego słowa - samo hyphens na nagłówku nic wtedy nie daje, bo
   kontener i tak trzyma szerokość. */
.reg-title {
  min-width: 0;
}

/* „Cyberbezpieczeństwa" ma 19 znaków i przy 320 px wychodziło o 32 px poza
   kadr - bez poziomego scrolla, więc końcówka nazwy po prostu znikała.
   Dzielenie po sylabach, a gdy i to nie wystarczy, twarde łamanie wyrazu:
   ucięta nazwa ustawy jest gorsza niż nazwa przełamana. */
.reg-title h3 {
  font-size: clamp(1.35rem, 2.6vw, 2.1rem);
  font-variation-settings: "wdth" 100;
  hyphens: auto;
  overflow-wrap: break-word;
}

.reg-kind {
  display: inline-block;
  margin-top: 0.5rem;
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  letter-spacing: var(--track-mono);
  text-transform: uppercase;
  color: var(--steel-dim);
}

.reg-body {
  display: grid;
  gap: clamp(1.25rem, 3vw, 2.5rem);
  grid-template-columns: minmax(0, 1fr);
}

@media (min-width: 60rem) {
  .reg-body {
    grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr);
  }
  .reg-body > p:only-child {
    grid-column: 1 / -1;
    max-width: 52rem;
  }
}

.reg-body p {
  color: var(--steel-bright);
  font-size: 0.9375rem;
  line-height: 1.75;
}

.reg-note {
  padding: 1.2rem 1.35rem;
  border-left: 2px solid var(--signal);
  background: rgba(255, 255, 255, 0.035);
  border-radius: 0 3px 3px 0;
}

.reg-note h4 {
  font-family: var(--font-mono);
  font-size: var(--step-mono);
  font-weight: 500;
  letter-spacing: var(--track-mono);
  text-transform: uppercase;
  color: var(--signal);
  margin-bottom: 0.7rem;
}

.reg-note p {
  font-size: 0.8125rem;
  line-height: 1.65;
}

/* Na wąskim ekranie karta jest wyższa niż okno - przyklejona uwięziłaby
   własną stopkę poza zasięgiem. Stos działa tylko tam, gdzie się mieści. */
@media (max-width: 60rem) {
  .reg {
    position: static;
    margin-bottom: 1.25rem;
  }
}

/* --- 03 · możliwości ------------------------------------------------------ */

/* --- 04 · pokład: przypięta scena 3D ------------------------------------- */

.onboard {
  position: relative;
  background: var(--ink);
}

/* Tor w vh, nie svh, celowo: vh jest stabilne, a svh zmienia się przy chowaniu
   paska adresu na mobile i przeliczałoby długość sekwencji w trakcie scrolla.
   Scena zostaje na 100svh, żeby zawsze wypełniała widoczny kadr. */
.onboard-track {
  height: 400vh;
  position: relative;
}

.onboard-stage {
  position: sticky;
  top: 0;
  height: 100svh;
  overflow: hidden;
}

/* Podpis siada na scenie o nieprzewidywalnej jasności - bez przyciemnienia
   pomarańczowy eyebrow ginie na jasnej szafie albo na białym pudle wagonu. */
.onboard-stage::before,
.csirt-stage::before {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 34vh;
  z-index: 1;
  pointer-events: none;
  background: linear-gradient(
    to bottom,
    rgba(8, 7, 26, 0.88) 0%,
    rgba(8, 7, 26, 0.5) 45%,
    transparent 100%
  );
}

/* Scena wygasza się do tła u dołu, żeby odpięcie sekcji nie ucinało kadru. */
.onboard-stage::after {
  content: "";
  position: absolute;
  inset: auto 0 0 0;
  height: 22vh;
  z-index: 2;
  pointer-events: none;
  background: linear-gradient(to top, var(--ink) 12%, transparent);
}

#onboard-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

.stage-caption {
  position: absolute;
  left: 0;
  right: 0;
  top: clamp(4.5rem, 12vh, 7rem);
  z-index: 3;
  text-align: center;
  pointer-events: none;
  padding-inline: 1.5rem;
}

.stage-caption .eyebrow {
  transition: opacity var(--dur-mid) var(--ease-out);
}

.stage-caption h2 {
  margin-top: 0.65rem;
  font-size: clamp(1.4rem, 3vw, 2.4rem);
}

.stage-steps {
  position: absolute;
  left: 50%;
  bottom: clamp(2.5rem, 7vh, 4.5rem);
  z-index: 3;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: 0.9rem;
  pointer-events: none;
}

.stage-steps li {
  display: flex;
  align-items: center;
  gap: 0.9rem;
  font-family: var(--font-mono);
  font-size: 0.625rem;
  letter-spacing: var(--track-mono);
  text-transform: uppercase;
  color: var(--steel-dim);
  transition: color var(--dur-mid) var(--ease-out);
}

.stage-steps li::before {
  content: "";
  width: 0.4rem;
  height: 0.4rem;
  border: 1px solid currentColor;
  border-radius: 50%;
  transition: background var(--dur-mid) var(--ease-out),
    box-shadow var(--dur-mid) var(--ease-out);
}

.stage-steps li.is-live {
  color: var(--signal);
}

.stage-steps li.is-live::before {
  background: var(--signal);
  box-shadow: 0 0 10px var(--glow-signal);
}

@media (max-width: 48rem) {
  .stage-steps li span {
    display: none;
  }
}

/* --- panel opisu urządzenia: wjeżdża, gdy kamera dojedzie na miejsce ----- */

.spec {
  position: absolute;
  z-index: 3;
  right: clamp(1.25rem, 4vw, 3.5rem);
  top: 50%;
  width: min(24rem, 40vw);
  padding: clamp(1.4rem, 2.2vw, 1.9rem);
  background: rgba(16, 14, 46, 0.82);
  backdrop-filter: blur(16px) saturate(150%);
  -webkit-backdrop-filter: blur(16px) saturate(150%);
  border: 1px solid var(--hair-dark);
  border-left: 2px solid var(--signal);
  border-radius: 3px;
  opacity: 0;
  transform: translate3d(1.5rem, -50%, 0);
  pointer-events: none;
  transition: opacity var(--dur-slow) var(--ease-out),
    transform var(--dur-slow) var(--ease-out);
}

.spec.is-shown {
  opacity: 1;
  transform: translate3d(0, -50%, 0);
  pointer-events: auto;
}

.spec-index {
  display: block;
  font-family: var(--font-mono);
  font-size: 0.625rem;
  letter-spacing: var(--track-mono);
  text-transform: uppercase;
  color: var(--signal);
  margin-bottom: 0.9rem;
}

.spec h3 {
  font-size: clamp(1.35rem, 2.2vw, 1.85rem);
  font-variation-settings: "wdth" 100;
  margin-bottom: 0.7rem;
}

.spec p {
  font-size: var(--step-small);
  line-height: 1.6;
  color: var(--steel-bright);
  margin: 0;
}

.spec-rows {
  margin-top: 1.3rem;
  padding-top: 1.1rem;
  border-top: 1px solid var(--hair-dark);
  display: grid;
  gap: 0.65rem;
}

.spec-rows > div {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 1rem;
}

.spec-rows dt,
.spec-rows dd {
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  margin: 0;
}

.spec-rows dt {
  color: var(--steel-dim);
  flex-shrink: 0;
}

.spec-rows dd {
  color: var(--white);
  text-align: right;
}

/* Na wąskim ekranie panel schodzi na dół; scena chowa się pod gradientem,
   więc opis nigdy nie ląduje na samym urządzeniu. */
@media (max-width: 60rem) {
  .spec {
    right: 1rem;
    left: 1rem;
    top: auto;
    bottom: clamp(3.5rem, 9vh, 5.5rem);
    width: auto;
    transform: translate3d(0, 1.5rem, 0);
  }

  .spec.is-shown {
    transform: translate3d(0, 0, 0);
  }

  .spec-rows {
    display: none;
  }
}

/* --- 05 · centrala: druga scena przypięta do scrolla -------------------- */

.csirt {
  position: relative;
  background: var(--ink);
}

.csirt-track {
  height: 320vh;
  position: relative;
}

.csirt-stage {
  position: sticky;
  top: 0;
  height: 100svh;
  overflow: hidden;
}

.csirt-stage::after {
  content: "";
  position: absolute;
  inset: auto 0 0 0;
  height: 22vh;
  z-index: 2;
  pointer-events: none;
  background: linear-gradient(to top, var(--ink) 12%, transparent);
}

#csirt-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

.csirt-outro {
  position: relative;
  padding-block: clamp(3rem, 8vh, 5rem) var(--pad-section);
}

.csirt-outro .lead {
  max-width: 40rem;
}

/* Szerokość wspólna z akapitem nad siatką: przy dwóch pozycjach zamiast trzech
   frakcje rozpychały je na całą sekcję i między „24/7" a „0" zostawało 470 px
   pustki, czytelne jako brakująca trzecia metryka. Ograniczenie idzie na cały
   blok, razem z kreską, bo linia krótsza od tekstu nad nią wygląda na decyzję,
   a linia pełnej szerokości nad zwartą grupą - na niedokończony układ. */
.metrics {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  max-width: 40rem;
  gap: 1.5rem 2rem;
  margin-top: clamp(2rem, 4vh, 2.75rem);
  padding-top: clamp(1.75rem, 4vh, 2.25rem);
  border-top: 1px solid var(--hair-dark);
}

.metric dt {
  font-family: var(--font-mono);
  font-size: 0.625rem;
  letter-spacing: var(--track-mono);
  text-transform: uppercase;
  color: var(--steel-dim);
  margin-bottom: 0.5rem;
}

.metric dd {
  margin: 0;
  font-family: var(--font-display);
  font-variation-settings: "wdth" var(--wdth-wide);
  font-size: clamp(1.75rem, 3.4vw, 2.6rem);
  font-weight: 700;
  line-height: 1;
  letter-spacing: var(--track-tight);
}

.metric dd em {
  font-style: normal;
  color: var(--signal);
}

/* --- przystanki na torze sceny --------------------------------------------
   Punkty, na których przewijanie ma się zatrzymać, żeby jedno długie
   pociągnięcie palcem nie przewinęło całej sekwencji. Powód i wszystkie
   bramki opisuje app.js, sekcja przystanków; tutaj zostaje sam mechanizm.

   Element jest wstawiany przez JS i nie ma nic wspólnego z układem strony:
   jego jedyną rolą jest dać przeglądarce pozycję do zaczepienia. Wysokość
   nadaje mu JS, bo długość toru zależy od wysokości okna.

   `mandatory` zapala się klasą tylko na czas przejazdu przez tor. Zostawione
   na stałe ciągnęłoby czytającego do najbliższego przystanku także wtedy,
   gdy zatrzymał się w środku akapitu w zupełnie innej sekcji.
   -------------------------------------------------------------------------- */

.scene-stop {
  position: absolute;
  left: 0;
  width: 1px;
  height: 1px;
  pointer-events: none;
  scroll-snap-align: start;
  scroll-snap-stop: always;
}

html.scene-snap {
  scroll-snap-type: y mandatory;
}

/* --- awaria sceny 3D ------------------------------------------------------
   Klasę wnosi JS, gdy three.js nie wstało, nie ma WebGL-a albo padła
   inicjalizacja strony. Bez tego przypięty tor zostaje jedenastoma ekranami
   czerni z napisem „Ładowanie sceny" - czyli stroną zepsutą, nie ograniczoną.
   Sekcja schodzi do wysokości treści, a panele opisu wracają do przepływu.

   Wariant „JS wyłączony całkowicie" jest w css/no-js.css, wciąganym przez
   <noscript>. NIE robimy tego klasą zakładaną przez skrypt: gdyby CSS zwijał
   sceny pod nieobecność takiej klasy, wystarczyłby jeden skrypt zjedzony przez
   cache albo proxy, żeby sceny zniknęły komuś, u kogo wszystko działa.
   -------------------------------------------------------------------------- */

.scene-off .onboard-track,
.scene-off .csirt-track {
  height: auto;
}

.scene-off .onboard-stage,
.scene-off .csirt-stage {
  position: static;
  height: auto;
  overflow: visible;
  width: min(100% - var(--gutter) * 2, var(--shell));
  margin-inline: auto;
  padding-left: var(--spine-w);
  padding-block: var(--pad-section);
}

/* gradienty przyciemniające kadr nie mają czego przyciemniać */
.scene-off .onboard-stage::before,
.scene-off .onboard-stage::after,
.scene-off .csirt-stage::before,
.scene-off .csirt-stage::after {
  display: none;
}

.scene-off canvas,
.scene-off .stage-steps {
  display: none;
}

.scene-off .scene-status {
  position: static;
  display: block;
  background: none;
  margin-bottom: 1.25rem;
}

.scene-off .stage-caption {
  position: static;
  top: auto;
  text-align: left;
  padding-inline: 0;
  pointer-events: auto;
}

.scene-off .spec {
  position: static;
  width: auto;
  max-width: 34rem;
  margin-top: 2rem;
  /* Bez tego opacity i transform zostają w gestii przejścia z .spec, a przejście
     nie musi wystartować - w karcie w tle zegar animacji stoi i panel zostaje
     niewidoczny. Treść awaryjna nie może zależeć od tego, czy coś się animuje. */
  transition: none;
  opacity: 1;
  transform: none;
  pointer-events: auto;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  background: var(--ink-raise);
}

.scene-off .spec-rows {
  display: grid;
}

/* --- 06 · kontakt --------------------------------------------------------- */

.contact-layout {
  display: grid;
  gap: clamp(2.5rem, 6vw, 5rem);
  grid-template-columns: minmax(0, 1fr);
}

@media (min-width: 62rem) {
  .contact-layout {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.1fr);
    align-items: start;
  }
}

.people {
  display: grid;
  gap: 1px;
  background: var(--hair-light);
  border: 1px solid var(--hair-light);
  border-radius: 3px;
  overflow: hidden;
}

.person {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: 1.25rem;
  align-items: center;
  padding: clamp(1.4rem, 3vw, 1.9rem);
  background: var(--paper-raise);
}

/* Zdjęcie, nie inicjały. Granatowe tło zostaje jako podkład na czas wczytywania
   i na wypadek, gdyby plik nie doszedł - pusty biały krąg wyglądałby na usterkę.
   object-fit przycina kadr do koła bez rozciągania twarzy. */
.person-mark {
  width: 3.5rem;
  height: 3.5rem;
  flex-shrink: 0;
  border-radius: 50%;
  background: var(--navy);
  object-fit: cover;
}

.person h3 {
  color: var(--navy);
  font-size: 1.05rem;
  margin-bottom: 0.15rem;
}

.person .role {
  font-family: var(--font-mono);
  font-size: 0.625rem;
  letter-spacing: var(--track-mono);
  text-transform: uppercase;
  color: var(--signal-dim);
  margin-bottom: 0.6rem;
}

.person a,
.person .tel {
  display: block;
  font-size: var(--step-small);
  color: var(--slate);
  transition: color var(--dur-fast) var(--ease-out);
}

.person a:hover {
  color: var(--signal-dim);
}

/* --- stopka --------------------------------------------------------------- */

.site-foot {
  background: var(--paper);
  color: var(--slate);
  border-top: 1px solid var(--hair-light);
  padding-block: 3rem;
}

.foot-inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  /* Pole ochronne wokół znaku: odstęp równy wysokości litery „P”. Przy logo
     szerokim na 200 px daje to ok. 30 px. */
  gap: 2rem;
}

/* Minimalna dopuszczalna szerokość znaku to 200 px. Stopka jest jedynym
   miejscem z pełnym lockupem, więc to ona musi ten limit spełnić.

   Kolejność w stopce jest odwrócona względem porządku w HTML: znak wychodzi
   na prawą krawędź, a linia z prawami na lewą. W drzewie dokumentu znak
   zostaje pierwszy, bo to on otwiera stopkę dla czytnika ekranu, a nota
   o prawach jest przypisem i ma się czytać na końcu. */
.foot-mark {
  width: 12.5rem;
  height: auto;
}

/* Znaki uczelni stoją po drugiej stronie stopki, pod własnym podpisem. Bez
   podpisu trzy znaki w rzędzie czytają się jak trzy firmy stojące za
   produktem, a rola Politechniki jest inna. */
.foot-partners {
  display: flex;
  flex-direction: column;
  gap: 0.9rem;
}

/* Podpis cofa się wielkością i światłem między literami, nie przezroczystością.
   Przy kryciu 0.75 kontrast schodził do 3.5:1, czyli poniżej progu WCAG AA
   i poniżej najsłabszego miejsca na całej stronie. */
.foot-partners-label {
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  letter-spacing: var(--track-mono);
  text-transform: uppercase;
  color: var(--slate);
}

/* Sygnet PK ma mieć 64 px wysokości - to minimum z księgi znaku dla ekranu
   (str. 7). W pliku sygnet zajmuje 302 z 426 px wysokości, bo resztę wypełnia
   wbudowane pole ochronne, więc żeby sygnet trafił w minimum, cały znak
   potrzebuje 90 px. To jest podłoga, nie docelowa wielkość.

   Docelową wyznacza równowaga ze znakiem PTS po drugiej stronie stopki. Tamten
   stoi na 200 px, czyli na własnym minimum z księgi (str. 6), i niżej zejść nie
   może - więc to znak uczelni musi podejść do niego, a nie odwrotnie. Zmierzone
   przy 6rem: rysunek PTS ma 105 px wysokości, rysunek PK tylko 68, bo prawie
   trzecią część ramki zjada wbudowane pole ochronne. Stąd 8rem: sygnet dostaje
   91 px, rysunek 91 px, przy rysunku PTS wysokim na 105.

   **8rem jest ostatnią wartością, która trzyma jedno i drugie ograniczenie,
   i żadne z nich nie stoi w księdze.** Pierwsze to hierarchia: rysunki obu
   znaków zrównują się przy 8.75rem, a powyżej partner naukowy czyta się
   mocniej niż właściciel strony. Drugie to szerokość rzędu - każdy rem zjada
   67 px w wersji polskiej i 74 px w angielskiej, więc przy 8.75rem zostaje
   6 px zapasu na progu 68rem i rząd zawija się przy pierwszym drobiazgu.
   Wartość doszła tutaj w trzech krokach, na kolejne prośby o „jeszcze
   odrobinę"; następna prośba nie jest już regulacją, tylko zmianą tego,
   który znak jest w stopce ważniejszy.

   Oba ograniczenia są na maksimum, a wymiary zostają na `auto`, żeby wąski
   ekran zmniejszał znak proporcjonalnie. Ingerencja w proporcje znaku jest
   zakazana wprost, a znak jest tu szeroki - 576 px w wersji polskiej i 632 px
   w angielskiej, bo napis jest dłuższy.

   Poniżej ~500 px szerokości ekranu sygnet schodzi pod 64 px i nie ma na to
   rady: umowa z Wydziałem dopuszcza wyłącznie ten jeden, poziomy wariant
   znaku, a wariantu symetrycznego - który zmieściłby się w kolumnie - nie
   wolno użyć. Znak zmniejsza się wtedy razem z ekranem, zamiast wystawać. */
.foot-partner-mark {
  width: auto;
  height: auto;
  max-width: 100%;
  max-height: 8rem;
}

/* Linia z prawami schodzi do własnego wiersza: w jednym rzędzie ze znakami
   zabierała im szerokość i ściskała je poniżej dopuszczalnego rozmiaru.
   Stąd własny wiersz zamiast wspólnego - przy lewej krawędzi, naprzeciw
   znaku PTS, który stoi teraz po prawej. */
.foot-inner p {
  flex-basis: 100%;
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--slate);
}

/* Przestawienie działa wyłącznie tam, gdzie stopka jest jednym rzędem. Zmierzone:
   znak PTS wskakuje obok znaków uczelni od 1060 px, niżej wszystko układa się
   w kolumnę. Bez tego progu na węższym ekranie znak PTS lądował POD znakami
   Politechniki, czyli właściciel strony pod partnerem naukowym. */
@media (min-width: 68rem) {
  .foot-mark {
    order: 3;
  }

  .foot-inner p {
    order: 4;
  }
}
