@charset "utf-8";

/* ==========================================================================
   Maciej Grela — arkusz stylów witryny

   Spis: 1. Kroje · 2. Tokeny · 3. Podstawy · 4. Pomocnicze · 5. Nagłówek
         6. Treść · 7. Projekty · 8. Kontakt · 9. Ruch · 10. Druk
   ========================================================================== */


/* ------------------------------------------------------------ 1. Kroje ---- */

@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url('../fonts/inter-400-latin-ext.woff2') format('woff2');
    unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url('../fonts/inter-400-latin.woff2') format('woff2');
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/*
 * Skala ogranicza się do dwóch grubości: 400 dla tekstu i 600 dla wyróżnień.
 * Obie są wstępnie pobierane, więc żaden napis nie zmienia grubości po wczytaniu.
 */

@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 600;
    font-display: swap;
    src: url('../fonts/inter-600-latin-ext.woff2') format('woff2');
    unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 600;
    font-display: swap;
    src: url('../fonts/inter-600-latin.woff2') format('woff2');
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/*
 * Krój wyświetleniowy tekstu wiodącego — odpowiednik ozdobnego serifu wzorca
 * (tam komercyjny Bethany Elingston, tu Cormorant Garamond na licencji OFL,
 * podawany z własnego serwera). Ten sam krój posłużył do wygenerowania krzywych
 * liter adresu na zakładce kontaktowej.
 */

@font-face {
    font-family: 'Cormorant Garamond';
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url('../fonts/cormorant-400-latin-ext.woff2') format('woff2');
    unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
    font-family: 'Cormorant Garamond';
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url('../fonts/cormorant-400-latin.woff2') format('woff2');
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}


/* ----------------------------------------------------------- 2. Tokeny ---- */

/*
 * Paleta odwzorowuje wzorzec jgthms.com. Jest zasadniczo bezbarwna: tła i teksty
 * to czyste szarości, a kolor pojawia się wyłącznie w kafelku ikony projektu
 * oraz na aktywnej opcji przełącznika motywu.
 *
 * Dwa odstępstwa od wartości wzorca, oba w tekście drugorzędnym (--text-weak).
 * Wzorzec używa tam #989898 w motywie jasnym i #6b6b6b w ciemnym, co daje
 * odpowiednio 2,9:1 i 3,5:1 — poniżej progu 4,5:1. Wartości zostały przyciemnione
 * do najbliższych spełniających normę; różnica jest ledwo dostrzegalna.
 *
 * Zieleń wzorca (#48c78e) ma na bieli 2,1:1, więc w motywie jasnym aktywna opcja
 * motywu korzysta z jej przyciemnionej odmiany.
 *
 * Motyw jasny stanowi podstawę. Ciemny nadpisywany jest dwukrotnie: według
 * ustawienia systemu oraz według wyboru zapisanego w przeglądarce.
 */

:root {
    --bg: #ffffff;
    --surface: #ffffff;
    --border: rgb(20 20 20 / 12%);
    --control-border: rgb(20 20 20 / 32%);

    --text-strong: #141414;
    --text: #717171;
    --text-weak: #767676;

    --shadow: rgb(0 0 0 / 10%);
    --raised: rgb(200 200 200 / 30%);
    --raised-strong: rgb(180 180 180 / 45%);

    --mark-bg: #e8e8e8;
    --mark-fg: #141414;

    --active: #2f9e6b;

    --header-bg: rgb(255 255 255 / 82%);

    /*
     * Układ wzorca opiera się na jednej skali: wszystkie rozmiary są wielokrotnością
     * rozmiaru bazowego, a ten rośnie wraz z szerokością okna od 16 px do 20 px.
     * Dzięki temu proporcje pozostają te same na telefonie i na monitorze,
     * a strona nie wymaga osobnych wartości dla każdego elementu.
     *
     * Margines boczny przełamuje się przy 864 px: poniżej jest wielokrotnością
     * rozmiaru bazowego, powyżej ułamkiem szerokości okna.
     */
    --pad-x: 2em;
    --pad-y: 2em;

    /*
     * Rozmiar tekstu wyświetlanego (wiodącego i tytułów podstron). Poniżej 54rem
     * stały, powyżej rośnie z szerokością okna — dokładnie jak we wzorcu, który
     * na wąskim ekranie nie powiększa go wcale.
     */
    --display-size: 2em;

    --radius-s: 0.5rem;
    --radius-m: 0.85rem;
    --radius-l: 1.25rem;

    /* Krój wyświetleniowy — patrz sekcja 1. */
    --family-display: 'Cormorant Garamond', Georgia, 'Times New Roman', serif;

    --dur: 210ms;
    --ease: cubic-bezier(0.2, 0.8, 0.2, 0.8);

    color-scheme: light dark;
}

@media (prefers-color-scheme: dark) {
    :root:not([data-theme='light']) {
        --bg: #141414;
        --surface: #1c1c1c;
        --border: rgb(245 245 245 / 12%);
        --control-border: rgb(245 245 245 / 32%);

        --text-strong: #f5f5f5;
        --text: #a3a3a3;
        --text-weak: #808080;

        --shadow: rgb(0 0 0 / 40%);
        --raised: rgb(255 255 255 / 12%);
        --raised-strong: rgb(255 255 255 / 20%);

        --mark-bg: #333333;
        --mark-fg: #ffffff;

        --active: #48c78e;

        --header-bg: rgb(20 20 20 / 82%);
    }
}

:root[data-theme='dark'] {
    --bg: #141414;
    --surface: #1c1c1c;
    --border: rgb(245 245 245 / 12%);
    --control-border: rgb(245 245 245 / 32%);

    --text-strong: #f5f5f5;
    --text: #a3a3a3;
    --text-weak: #808080;

    --shadow: rgb(0 0 0 / 40%);
    --raised: rgb(255 255 255 / 12%);
    --raised-strong: rgb(255 255 255 / 20%);

    --mark-bg: #333333;
    --mark-fg: #ffffff;

    --active: #48c78e;

    --header-bg: rgb(20 20 20 / 82%);
}


/* ---------------------------------------- barwy kafelków ikon projektów ---- */

/*
 * Jedyne miejsce w interfejsie, gdzie występuje pełna barwa. Wartości pochodzą
 * z wzorca; każdą zestawiono ze znakiem o kontraście co najmniej 3:1 wobec tła
 * kafelka. Barwę wybiera się w pliku danych projektu polem tile.
 */

.project__icon--slate { --tile-bg: #333333; --tile-fg: #ffffff; }
.project__icon--red   { --tile-bg: #ff3860; --tile-fg: #ffffff; }
.project__icon--blue  { --tile-bg: #24b1d1; --tile-fg: #141414; }
.project__icon--green { --tile-bg: #48c78e; --tile-fg: #141414; }
.project__icon--sun   { --tile-bg: #efba2b; --tile-fg: #141414; }


/* --------------------------------------------------------- 3. Podstawy ---- */

*,
*::before,
*::after {
    box-sizing: border-box;
}

html {
    -webkit-text-size-adjust: 100%;
    scroll-behavior: smooth;

    /* Skala bazowa całej witryny: 16 px na wąskim ekranie, 20 px od ok. 1120 px. */
    font-size: clamp(1rem, 0.9rem + 0.5vw, 1.25rem);

    /*
     * Pas zarezerwowany dla przyklejonego nagłówka. Bez niego element, który właśnie
     * otrzymał zaznaczenie klawiaturą, potrafił wylądować pod nagłówkiem razem ze
     * swoją obwódką — przeglądarka uznawała go za widoczny, bo mieścił się w oknie.
     * To wymóg WCAG 2.2, kryterium 2.4.11. Odjemnik odpowiada wysokości nagłówka
     * (dwa marginesy pionowe i kafelek znaku), powiększonej o zapas na obwódkę.
     * Ta sama wartość porządkuje skok pod kotwicę „przejdź do treści”.
     *
     * Zapas 2rem, a nie 1rem: sama obwódka wystaje 5 px ponad element (2 px kreski
     * i 3 px odstępu), a wysokość nagłówka zmienia się skokowo wraz z zawijaniem
     * nawigacji — przy 480 px zapas jednego rema schodził do czterech pikseli.
     */
    scroll-padding-top: calc(2 * var(--pad-y) + 2.25em + 2rem);
}

/* Od tej szerokości margines boczny liczy się od szerokości okna, nie od skali. */
@media (width >= 54rem) {
    :root {
        --pad-x: 5vw;
        --pad-y: 5vw;
        --display-size: clamp(2em, 2em + 2vw, 4em);
    }
}

body {
    margin: 0;
    min-height: 100svh;
    display: flex;
    flex-direction: column;

    background-color: var(--bg);
    color: var(--text);

    font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
    font-size: 1rem;
    font-weight: 400;
    line-height: 1.5;
    font-feature-settings: 'liga' 1, 'calt' 1;
}

h1,
h2,
h3 {
    margin: 0;
    color: var(--text-strong);
    font-weight: 600;
    line-height: 1.2;
    letter-spacing: -0.015em;
    text-wrap: balance;
}

p {
    margin: 0;
    text-wrap: pretty;
}

ul {
    margin: 0;
    padding: 0;
    list-style: none;
}

svg {
    display: block;
}

a {
    color: inherit;
    text-decoration: none;
}

button {
    font: inherit;
    color: inherit;
}

:focus-visible {
    outline: 2px solid var(--text-strong);
    outline-offset: 3px;
    border-radius: 2px;
}

/*
 * Atrybut hidden musi wygrywać z regułami układu. Bez tego reguła .button
 * z własnością display przesłania wbudowany styl przeglądarki i element
 * oznaczony jako ukryty pozostaje widoczny.
 */
[hidden] {
    display: none !important;
}


/* ------------------------------------------------------- 4. Pomocnicze ---- */

.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

.skip-link {
    position: absolute;
    top: 0;
    left: 50%;
    z-index: 100;
    transform: translate(-50%, -120%);

    padding: 0.6rem 1.1rem;
    border-radius: 0 0 var(--radius-s) var(--radius-s);
    background-color: var(--text-strong);
    color: var(--bg);
    font-weight: 600;
    transition: transform var(--dur) var(--ease);
}

.skip-link:focus-visible {
    transform: translate(-50%, 0);
    outline-offset: -4px;
}

/*
 * Animowane podkreślenie odnośników — motyw przewodni wzorca. Kreska przyjmuje
 * barwę tekstu, a nie osobnego akcentu: we wzorcu nie ma barwy wiodącej.
 */
.link {
    position: relative;
}

.link::after {
    content: '';
    position: absolute;
    inset-inline: 0;
    bottom: -0.25em;
    height: 1px;
    border-radius: 999px;
    background-color: currentColor;
    transform: scaleX(0);
    transform-origin: right center;
    transition: transform var(--dur) var(--ease);
}

.link[aria-current='page']::after {
    transform: scaleX(1);
    transform-origin: left center;
}

@media (hover: hover) {
    .link:hover::after {
        transform: scaleX(1);
        transform-origin: left center;
    }
}

.link:focus-visible::after {
    transform: scaleX(1);
    transform-origin: left center;
}

/* Przyciski */
.button {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;

    padding: 0.55rem 1rem;
    border: 1px solid transparent;
    border-radius: 999px;

    font-size: 0.875em;
    font-weight: 500;
    line-height: 1.2;
    white-space: nowrap;
    cursor: pointer;

    transition:
        background-color var(--dur) var(--ease),
        border-color var(--dur) var(--ease),
        color var(--dur) var(--ease),
        transform var(--dur) var(--ease);
}

.button .icon {
    width: 1.05em;
    height: 1.05em;
    flex: none;
}

.button__trail {
    opacity: 0.75;
}

/*
 * Przycisk główny to półprzezroczysta pigułka z rozmyciem tła — rozwiązanie
 * z wzorca, gdzie działanie wyróżnia się fakturą, a nie barwą.
 */
.button--primary {
    background-color: var(--raised);
    color: var(--text-strong);
    backdrop-filter: blur(4px);
    -webkit-backdrop-filter: blur(4px);
}

@media (hover: hover) {
    .button--primary:hover {
        background-color: var(--raised-strong);
        transform: translateY(-1px);
    }
}

.button:active {
    transform: translateY(0);
}


/* -------------------------------------------------------- 5. Nagłówek ---- */

.site-header {
    position: sticky;
    top: 0;
    z-index: 30;

    display: flex;
    align-items: center;
    gap: 1.5em;
    justify-content: space-between;

    padding-block: var(--pad-y);
    padding-inline: var(--pad-x);
}

/*
 * Tło i rozmycie nagłówka wygasają z góry na dół: pełna siła przy górnej
 * krawędzi, połowa przy dolnej. Maska obejmuje także efekt rozmycia tła,
 * więc nagłówek przechodzi w treść płynnie, bez linii oddzielającej.
 */
.site-header::before {
    content: '';
    position: absolute;
    inset: 0;
    z-index: -1;

    background-color: var(--header-bg);
    backdrop-filter: blur(12px);
    -webkit-backdrop-filter: blur(12px);
    -webkit-mask-image: linear-gradient(to bottom, rgb(0 0 0), rgb(0 0 0 / 50%));
    mask-image: linear-gradient(to bottom, rgb(0 0 0), rgb(0 0 0 / 50%));
}

/*
 * Znak firmowy nie może się kurczyć. Wcześniej pochłaniał cały niedomiar szerokości
 * nagłówka i schodził do zera, przez co kafelek monogramu — mający flex: none —
 * wychodził poza rodzica i nachodził na pierwszy odnośnik nawigacji.
 */
.brand {
    display: flex;
    align-items: center;
    gap: 0.7rem;
    flex: none;
    min-width: 0;
}

.brand__mark {
    display: grid;
    place-items: center;
    flex: none;

    /* 3em — tyle samo co znak autora we wzorcu. */
    width: 3em;
    height: 3em;
    border-radius: 0.75em;

    background-color: var(--mark-bg);
    color: var(--mark-fg);

    /* Postać zapasowa, gdy brakuje pliku z krzywą monogramu. */
    font-size: 0.75em;
    font-weight: 600;
    letter-spacing: 0.02em;

    transition: transform var(--dur) var(--ease);
}

/*
 * Monogram jest krzywą, a nie tekstem — krój odręczny nie jest wgrywany na serwer.
 * Znak zajmuje 82% boku kafelka, tyle samo co w favikonie i na obrazku podglądu.
 *
 * Obrys w kolorze wypełnienia odtwarza grubszą odmianę kroju: Dancing Script jest
 * dostępny wyłącznie jako plik zmienny, z którego dało się odczytać grubość 400.
 * Wartość 30 odnosi się do jednostek układu współrzędnych ścieżki, więc skaluje
 * się razem ze znakiem i jest identyczna we wszystkich rozmiarach.
 */
.brand__glyph {
    width: 82%;
    height: 82%;
    fill: currentColor;
    stroke: currentColor;
    stroke-width: 30;
    stroke-linejoin: round;
    stroke-linecap: round;
}

@media (hover: hover) {
    .brand:hover .brand__mark {
        transform: rotate(-6deg) scale(1.05);
    }
}

/* Barwa jawnie, bo nazwisko jest wyróżnieniem, a nie nagłówkiem — nie obejmuje
   go reguła h1/h2/h3 nadająca barwę tekstu wyróżnionego. */
.brand__name {
    margin: 0;
    color: var(--text-strong);
    font-size: 1em;
    font-weight: 500;
    letter-spacing: -0.01em;
    line-height: 1.25;
    white-space: nowrap;
}

.nav__list {
    display: flex;
    gap: 1.5em;
}

.nav__link {
    font-size: 0.875em;
    font-weight: 400;
    color: var(--text-weak);
    transition: color var(--dur) var(--ease);
}

.nav__link[aria-current='page'] {
    color: var(--text-strong);
    font-weight: 600;
}

@media (hover: hover) {
    .nav__link:hover {
        color: var(--text-strong);
    }
}

.controls {
    display: flex;
    align-items: center;
    gap: 1em;
}

/* Przełącznik motywu wymaga skryptów — bez nich pozostaje ukryty. */
.themes {
    display: none;
    gap: 0.1rem;
}

:root[data-js] .themes {
    display: flex;
}

.theme-option {
    display: grid;
    place-items: center;

    width: 1.95rem;
    height: 1.95rem;
    padding: 0;
    border: 0;
    border-radius: var(--radius-s);

    background-color: transparent;
    color: var(--text);
    /*
     * Wzorzec wygasza opcje nieaktywne do krycia 0,3, co daje ok. 1,5:1 — o wiele
     * za mało dla elementu sterującego. Krycie 0,8 zachowuje ten sam zamysł
     * wizualny przy 3,3:1 w motywie jasnym i 5,1:1 w ciemnym.
     */
    opacity: 0.8;
    cursor: pointer;

    transition:
        opacity var(--dur) var(--ease),
        color var(--dur) var(--ease),
        background-color var(--dur) var(--ease);
}

.theme-option .icon {
    width: 1.05rem;
    height: 1.05rem;
}

/* Jedyne miejsce w samym interfejsie, gdzie występuje barwa — tak jak we wzorcu. */
.theme-option[aria-pressed='true'] {
    opacity: 1;
    color: var(--active);
}

@media (hover: hover) {
    .theme-option:hover {
        opacity: 1;
        color: var(--text-strong);
    }
}

.lang-switch {
    font-size: 0.75em;
    font-weight: 600;
    letter-spacing: 0.06em;
    color: var(--text-weak);
    transition: color var(--dur) var(--ease);
}

@media (hover: hover) {
    .lang-switch:hover {
        color: var(--text-strong);
    }
}

/*
 * Wąski ekran. Nazwisko ustępuje miejsca, odstępy maleją, a gdyby i to nie
 * wystarczyło — wiersz nagłówka się zawija zamiast wypychać elementy poza margines.
 * Wcześniej trzy grupy próbowały zmieścić się w jednym wierszu za wszelką cenę,
 * przez co przy 320 px wersja angielska dostawała poziome przewijanie całej strony.
 */
@media (width < 30rem) {
    /*
     * Zawinięty nagłówek zajmuje dwa wiersze, więc pas rezerwowany dla zaznaczenia
     * musi urosnąć razem z nim — inaczej poprawka z sekcji 3 przestaje działać
     * dokładnie tam, gdzie ekran jest najciaśniejszy.
     */
    :root {
        scroll-padding-top: calc(2 * var(--pad-y) + 6.5em);
    }

    .site-header {
        flex-wrap: wrap;
        gap: 0.75em 1em;
    }

    .brand__name {
        display: none;
    }

    .nav__list {
        gap: 1em;
    }
}


/* ----------------------------------------------------------- 6. Treść ---- */

/*
 * Główny obszar treści nie niesie własnych marginesów — każda sekcja ma swoje,
 * tak jak we wzorcu. Dzięki temu tekst wiodący może wypełnić pierwszy ekran,
 * a rozsypany adres na zakładce kontaktowej wycentrować się w wolnej przestrzeni.
 */
.main {
    flex: 1;
    display: flex;
    flex-direction: column;
}

/*
 * Tekst wiodący wzorca to jeden duży, jednorodny napis krojem szeryfowym:
 * grubość 400, interlinia 1,375, wyrównany do lewej i wycentrowany w pionie
 * pierwszego ekranu. Odjemnik w min-height to wysokość nagłówka witryny —
 * dwa marginesy pionowe oraz znak o boku 3em przy rozmiarze pisma 0,75em
 * (czyli 2,25em w skali strony).
 */
.hero {
    display: flex;
    flex-direction: column;
    justify-content: center;
    min-height: calc(100svh - 2 * var(--pad-y) - 2.25em);
    padding: var(--pad-y) var(--pad-x);
}

.hero__title {
    color: var(--text-strong);
    font-family: var(--family-display);
    font-size: var(--display-size);
    font-weight: 400;
    line-height: 1.375;
    letter-spacing: normal;
    text-wrap: pretty;
}

.hero__line {
    display: block;
}

/* Odstęp między akapitami — pół firetu, wielkość z wzorca. */
.hero__line + .hero__line {
    margin-top: 0.5em;
}

.notfound {
    padding: 4em var(--pad-x) 5em;
}

/*
 * Nagłówek podstrony w postaci dyskretnej etykiety. Nazwa zakładki widnieje już
 * w nawigacji, więc powtarzanie jej dużym krojem wyświetleniowym przytłoczyłoby
 * treść — a nagłówek pierwszego poziomu musi być obecny i widoczny.
 */
.section-heading {
    font-size: 0.75em;
    font-weight: 500;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--text-weak);
}

/* Na stronie błędu nagłówek jest jedyną treścią, więc dostaje pełną skalę. */
.notfound .section-heading {
    font-family: var(--family-display);
    font-size: var(--display-size);
    font-weight: 400;
    letter-spacing: normal;
    line-height: 1.375;
    text-transform: none;
    color: var(--text-strong);
}

.lead {
    margin-top: 1em;
    max-width: 52ch;
    color: var(--text-weak);
    font-size: 1em;
}



/* -------------------------------------------------------- 7. Projekty ---- */

/* Zakładka Portfolio: nagłówek, filtr i lista. */
.portfolio {
    padding: 4em var(--pad-x) 5em;
}

.portfolio__pusto {
    margin-top: 2em;
    color: var(--text-weak);
}

/* ------------------------------------------------ filtr kategorii ---- */

/*
 * Element rozwijalny oparty na znaczniku details. Rozwija się natywnie, więc
 * działa bez skryptów i bez obsługi klawiatury pisanej ręcznie; skrypt dokłada
 * wyłącznie zamykanie klawiszem Esc i kliknięciem obok. Wewnątrz są zwykłe
 * odnośniki, bo każda kategoria ma własny adres.
 */
.filtr {
    position: relative;
    margin-top: 2em;
    width: fit-content;
    max-width: 100%;
}

.filtr__przycisk {
    display: flex;
    align-items: center;
    gap: 0.5em;

    padding: 0.55rem 1rem;
    border: 1px solid var(--control-border);
    border-radius: 999px;

    font-size: 0.875em;
    line-height: 1.2;
    color: var(--text-strong);
    cursor: pointer;
    user-select: none;
    white-space: nowrap;

    transition:
        border-color var(--dur) var(--ease),
        background-color var(--dur) var(--ease);
}

/* Własny znacznik zamiast trójkąta przeglądarki. */
.filtr__przycisk::-webkit-details-marker { display: none; }
.filtr__przycisk::marker { content: ''; }

.filtr__etykieta {
    color: var(--text-weak);
}

.filtr__etykieta::after {
    content: ':';
}

.filtr__wybor {
    font-weight: 500;
}

.filtr__strzalka {
    width: 1em;
    height: 1em;
    flex: none;
    color: var(--text-weak);
    transition: transform var(--dur) var(--ease);
}

.filtr[open] .filtr__strzalka {
    transform: rotate(180deg);
}

@media (hover: hover) {
    .filtr__przycisk:hover {
        border-color: var(--text-weak);
        background-color: var(--raised);
    }
}

/*
 * Lista wysuwa się nad treść. Pozycjonowanie bezwzględne, żeby otwarcie filtra
 * nie przesuwało listy projektów w dół.
 */
/*
 * Zamknięty filtr chowa listę jawnie. Przeglądarki robią to i bez tej reguły, ale
 * każda inaczej: starsze regułą display:none na dzieciach znacznika details, nowsze
 * własnością content-visibility na wewnętrznym pseudoelemencie. Ta druga zostawia
 * elementowi zmierzalny prostokąt, co przy pozycjonowaniu bezwzględnym bywa mylące
 * i zależy od wersji. Zapis wprost usuwa tę niejednoznaczność.
 */
.filtr:not([open]) .filtr__lista {
    display: none;
}

.filtr__lista {
    position: absolute;
    top: calc(100% + 0.5em);
    left: 0;
    z-index: 20;
    min-width: 100%;

    padding: 0.4em;
    border: 1px solid var(--border);
    border-radius: var(--radius-m);
    background-color: var(--surface);
    box-shadow: 0 0.75rem 2.5rem var(--shadow);
}

.filtr__pozycja {
    display: block;
    padding: 0.5em 0.85em;
    border-radius: var(--radius-s);

    font-size: 0.875em;
    line-height: 1.3;
    color: var(--text);
    white-space: nowrap;

    transition:
        background-color var(--dur) var(--ease),
        color var(--dur) var(--ease);
}

.filtr__pozycja[aria-current] {
    color: var(--text-strong);
    font-weight: 500;
    background-color: var(--raised);
}

@media (hover: hover) {
    .filtr__pozycja:hover {
        background-color: var(--raised);
        color: var(--text-strong);
    }
}

/*
 * Wąski ekran: lista rozciąga się na całą szerokość rodzica, żeby długie nazwy
 * kategorii nie wychodziły poza krawędź okna.
 */
@media (width < 30rem) {
    .filtr {
        width: 100%;
    }

    .filtr__przycisk {
        justify-content: space-between;
    }

    .filtr__pozycja {
        white-space: normal;
    }
}

/*
 * Ujemny margines boczny to rozwiązanie z wzorca: tekst wierszy pozostaje w linii
 * z resztą treści, a tło pojawiające się pod kursorem sięga poza nią, dzięki czemu
 * nie wygląda na ciasno przyklejone do liter.
 */
.projects {
    margin-top: 2em;
    margin-inline: -1em;
    display: flex;
    flex-direction: column;
    gap: 1em;
}

/*
 * Cztery kolumny: kafelek ikony, tytuł z opisem, etykiety, rocznik z przyciskiem.
 * Etykiety obok opisu, a nie pod nim — dzięki temu wiersz jest niski i czyta się
 * jednym spojrzeniem, tak jak we wzorcu. Poniżej 54rem układ się rozkłada.
 */
.project {
    position: relative;
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto auto;
    align-items: center;
    gap: 1em 1.5em;

    padding: 1em;
    border-radius: 1em;

    transition:
        background-color calc(var(--dur) * 1.4) var(--ease),
        box-shadow calc(var(--dur) * 1.4) var(--ease),
        filter calc(var(--dur) * 1.4) var(--ease),
        opacity calc(var(--dur) * 1.4) var(--ease),
        transform calc(var(--dur) * 1.4) var(--ease);
}

.project__icon {
    display: grid;
    place-items: center;

    /* 4em — tyle samo co kafelek projektu we wzorcu. */
    width: 4em;
    height: 4em;
    border-radius: 0.75em;

    /* Barwę wnosi klasa .project__icon--nazwa, dobierana w pliku danych projektu. */
    background-color: var(--tile-bg, #333333);
    color: var(--tile-fg, #ffffff);

    transition: transform var(--dur) var(--ease);
}

.project__icon .icon {
    width: 1.75em;
    height: 1.75em;
}

/*
 * Tytuły, opisy i etykiety pochodzą z pliku danych, więc mogą zawierać długi
 * ciąg bez spacji. Bez łamania wewnątrz wyrazu taki ciąg rozpycha siatkę
 * i wprowadza poziome przewijanie całej strony.
 */
.project__title {
    font-size: 1.25em;
    font-weight: 500;
    line-height: 1.25;
    letter-spacing: -0.01em;
    overflow-wrap: break-word;
}

.project__lead {
    margin-top: 0.25em;
    max-width: 60ch;
    color: var(--text-weak);
    font-size: 0.875em;
    line-height: 1.375;
    overflow-wrap: break-word;
}

.tags {
    display: flex;
    flex-wrap: wrap;
    align-content: center;
    gap: 0 0.5em;
    /* Proporcja kolumn zbliżona do wzorca: opis dostaje mniej więcej trzy razy
       więcej miejsca niż etykiety. */
    max-width: 13em;
}

/*
 * Etykiety we wzorcu nie mają ani obramowania, ani tła — to sam tekst w barwie
 * drugorzędnej, rozdzielony odstępem. Stąd większa przerwa między nimi.
 */
.tag {
    min-width: 0;
    max-width: 100%;

    color: var(--text-weak);
    font-size: 0.75em;
    font-weight: 500;
    line-height: 1.5;
    overflow-wrap: break-word;

    transition: color var(--dur) var(--ease);
}

/* Ukośnik rozdzielający etykiety — rozwiązanie z wzorca, zapisane regułą,
   żeby nie zaśmiecać znaczników treścią czysto ozdobną. */
.tag + .tag::before {
    content: '/';
    margin-right: 0.5em;
    opacity: 0.55;
}

.project__aside {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 0.5em;
}

@media (hover: hover) {
    /*
     * We wzorcu karta pod kursorem unosi się cieniem, a nie zmianą barwy —
     * w motywie jasnym powierzchnia jest tego samego koloru co tło.
     */
    .project:hover {
        background-color: var(--surface);
        box-shadow: 0 0.75rem 2.5rem var(--shadow);
    }

    .project:hover .project__icon {
        transform: scale(1.04);
    }

    .project:hover .tag {
        color: var(--text);
    }

    /*
     * Najechanie na jedną pozycję wycisza pozostałe — im dalej od wskazanej,
     * tym mocniej. Efekt z wzorca, ograniczony do wskaźnika precyzyjnego.
     */
    .projects:has(.project:hover) .project:not(:hover) {
        filter: blur(calc(var(--distance, 3) * 1px)) grayscale(1);
        opacity: 0.5;
    }

    .project:hover + .project,
    .projects .project:has(+ .project:hover) {
        --distance: 1;
    }

    .project:hover + .project + .project,
    .projects .project:has(+ .project + .project:hover) {
        --distance: 2;
    }
}

/*
 * Poniżej tej szerokości cztery kolumny się nie mieszczą: etykiety wracają pod
 * opis, a rocznik z przyciskiem schodzą do osobnego wiersza na całą szerokość.
 */
@media (width < 54rem) {
    .project {
        grid-template-columns: auto minmax(0, 1fr);
        align-items: start;
        row-gap: 0.75em;
    }

    .tags {
        grid-column: 2;
        max-width: none;
    }

    .project__aside {
        grid-column: 1 / -1;
        flex-direction: row;
        align-items: center;
        justify-content: flex-end;
        width: 100%;
    }
}


/* -------------------------------------------------------- 8. Kontakt ---- */

/*
 * Układ z wzorca: strona kontaktowa to wyłącznie rozsypany adres, wycentrowany
 * w wolnej przestrzeni pod nagłówkiem witryny — bez nagłówka sekcji, ramki
 * i przycisków. Odjemnik w min-height odpowiada wysokości nagłówka witryny.
 */
.contact {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: calc(100svh - 2 * var(--pad-y) - 2.25em);
    padding: var(--pad-y) var(--pad-x);
    overflow: hidden;
}

/*
 * Wysokość liter dobrana tak, by cały adres (proporcja szerokości do wysokości
 * ok. 7,6:1) mieścił się w oknie z zapasem na rozrzut, a na dużym ekranie nie
 * przekraczał pięciokrotności rozmiaru bazowego.
 */
.mail {
    --mail-h: min(5em, calc((100vw - 2 * var(--pad-x)) / 7.6));
}

.mail__letters {
    display: flex;
    align-items: flex-start;
    color: var(--text-strong);
}

.mail__item {
    display: block;
}

/*
 * Animacja wejściowa — litery wlatują z losowej rozsypki (--sx, --sy nadaje
 * warstwa skryptowa) na swoje miejsca, jedna po drugiej. Bez skryptów klasa
 * is-armed nie występuje: adres pozostaje złożony i czytelny, a dla automatów
 * zbierających dane to nadal wyłącznie krzywe, nie tekst.
 */
.mail.is-armed .mail__item {
    animation: mail-in calc(4 * var(--dur)) ease-out both;
    animation-delay: calc(var(--i, 0) * 80ms);
}

@keyframes mail-in {
    from {
        opacity: 0;
        transform: translate3d(var(--sx, 0px), var(--sy, 0px), 0);
    }

    to {
        opacity: 1;
        transform: translate3d(0, 0, 0);
    }
}

/*
 * Ciągłe śledzenie kursora — mechanizm wzorca. Warstwa skryptowa przelicza
 * każdy ruch myszy na przesunięcie względem środka napisu (--mx, --my),
 * a każda litera mnoży je przez własny losowy współczynnik (--fx, --fy).
 * Im bliżej środka napisu wskaźnik, tym adres czytelniejszy; stanu końcowego
 * nie ma. Narastający czas przejścia kolejnych liter daje efekt falowania.
 */
.mail__item svg {
    height: var(--mail-h);
    width: auto;
    overflow: visible;

    transform: translate3d(
        calc(var(--mx, 0px) * var(--fx, 1)),
        calc(var(--my, 0px) * var(--fy, 1)),
        0
    );
    transition: transform calc(2 * var(--dur)) ease-out;
    transition-duration: calc(var(--i, 0) * 20ms);
}

.mail__item path {
    fill: currentColor;
}


/* ----------------------------------------------------------- 9. Ruch ---- */

/*
 * Stan wyjściowy animacji wejścia zakładany jest wyłącznie przy działających
 * skryptach. Bez nich treść jest od razu widoczna.
 *
 * Znacznik data-reveal jest osobny od data-js, choć oba ustawia ten sam plik.
 * Powód: data-js steruje elementami, które bez skryptów nie mają sensu, i musi
 * pozostać założony na stałe, a data-reveal UKRYWA treść i dlatego ma w
 * theme-init.js licznik ratunkowy zdejmujący go, gdyby site.js nigdy nie ruszył.
 */
:root[data-reveal] .reveal {
    opacity: 0;
    transform: translateY(14px);
}

:root[data-reveal] .reveal.is-in {
    opacity: 1;
    transform: none;
    transition:
        opacity 520ms var(--ease),
        transform 520ms var(--ease);
    transition-delay: calc(var(--i, 0) * 80ms);
}

/*
 * Kaskada opóźnień na liście projektów. Zapisana regułami zamiast wartością
 * w znaczniku, żeby nie osłabiać polityki bezpieczeństwa treści.
 */
.projects .project:nth-child(2) { --i: 1; }
.projects .project:nth-child(3) { --i: 2; }
.projects .project:nth-child(4) { --i: 3; }
.projects .project:nth-child(5) { --i: 4; }
.projects .project:nth-child(6) { --i: 5; }
.projects .project:nth-child(7) { --i: 6; }
.projects .project:nth-child(8) { --i: 7; }
.projects .project:nth-child(9) { --i: 8; }
.projects .project:nth-child(n + 10) { --i: 9; }

@keyframes header-in {
    from {
        opacity: 0;
        transform: translateY(-10px);
    }

    to {
        opacity: 1;
        transform: none;
    }
}

:root[data-js] .site-header > * {
    animation: header-in 520ms var(--ease) both;
}

:root[data-js] .site-header > *:nth-child(2) {
    animation-delay: 70ms;
}

:root[data-js] .site-header > *:nth-child(3) {
    animation-delay: 140ms;
}

@media (prefers-reduced-motion: reduce) {
    html {
        scroll-behavior: auto;
    }

    /*
     * Opóźnienia trzeba wyzerować razem z czasami trwania. Samo skrócenie animacji
     * nie wystarcza: element z opóźnieniem nadal pojawia się po swoim czasie, więc
     * kaskada dwudziestu kilku liter adresu rozciągała się na blisko dwie sekundy
     * mimo „natychmiastowych” animacji.
     */
    *,
    *::before,
    *::after {
        animation-duration: 0.01ms !important;
        animation-delay: 0s !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
        transition-delay: 0s !important;
    }

    :root[data-reveal] .reveal {
        opacity: 1;
        transform: none;
    }

    /*
     * Przy ograniczonym ruchu warstwa skryptowa nie nadaje literom adresu ani
     * rozrzutu, ani śledzenia kursora — reguł do przesłonięcia tu nie ma.
     */
}


/* ----------------------------------------------------------- 10. Druk ---- */

@media print {
    /*
     * Tokeny trzeba wyzerować jawnie, obejmując wariant ciemny. Wybór motywu jest
     * odtwarzany z pamięci przeglądarki przed pierwszym renderowaniem i obowiązuje
     * także przy druku, a reguła :root[data-theme='dark'] ma wyższą wagę niż gołe
     * :root. Bez tego nagłówki (#f5f5f5) trafiłyby na wymuszone białe tło, dając
     * kontrast ok. 1,1:1 — tekst praktycznie niewidoczny na kartce.
     */
    :root,
    :root[data-theme='dark'],
    :root[data-theme='light'] {
        --bg: #ffffff;
        --surface: #ffffff;
        --border: rgb(20 20 20 / 25%);
        --control-border: rgb(20 20 20 / 45%);

        --text-strong: #000000;
        --text: #333333;
        --text-weak: #555555;

        --shadow: transparent;
        --raised: transparent;
        --raised-strong: transparent;

        --mark-bg: #e8e8e8;
        --mark-fg: #141414;
    }

    .site-header,
    .skip-link {
        display: none;
    }

    body {
        background: #fff;
        color: #000;
    }

    /*
     * Na wydruku adres ma być złożony i nieruchomy niezależnie od stanu na
     * ekranie. Zmienne --sx/--sy oraz --mx/--my są ustawiane stylem osadzonym,
     * ale transform czytany jest z reguł arkusza — wystarczy go tu przesłonić.
     */
    .mail.is-armed .mail__item {
        animation: none;
    }

    .mail__item svg {
        transform: none;
        transition: none;
    }
}
