* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

:root {
    --fg: #3F3A39;
    --bg: #EDF2F4;
    /* Secondary text: the lowest opacity that still gives 4.5:1 contrast on --bg. */
    --muted: 0.74;
}

html {
    font-size: 13px;
    -webkit-text-size-adjust: 100%;
    scrollbar-width: thin;
    scrollbar-color: rgb(63 58 57 / 0.25) transparent;
}

::-webkit-scrollbar {
    width: 6px;
}

::-webkit-scrollbar-thumb {
    background: rgb(63 58 57 / 0.25);
    border-radius: 3px;
}
body {
    font-family: 'Geist', system-ui, sans-serif;
    font-weight: 400;
    line-height: 1.5;
    background: var(--bg);
    color: var(--fg);
    -webkit-font-smoothing: antialiased;
    font-feature-settings: 'tnum';
}

::selection {
    background: var(--fg);
    color: var(--bg);
}

a {
    color: inherit;
    text-decoration: none;
    text-underline-offset: 3px;
    text-decoration-thickness: 1px;
}

/* Cursor from the favicon arrow, mirrored (↖); over anything clickable it tilts around its tip,
   and shrinks towards the tip while pressed. The tip stays put, so the hotspot is shared.
   Zoom cursors are magnifiers, set at the end of the file. */
@media (hover: hover) and (pointer: fine) {
    html {
        cursor: url("img/cursor/cursor.png") 1 1, auto;
        cursor: image-set(url("img/cursor/cursor.png") 1x, url("img/cursor/cursor@2x.png") 2x) 1 1, auto;
    }

    a, button, .art canvas {
        cursor: url("img/cursor/cursor-pointer.png") 1 1, pointer;
        cursor: image-set(url("img/cursor/cursor-pointer.png") 1x, url("img/cursor/cursor-pointer@2x.png") 2x) 1 1, pointer;
    }

    a:active, button:active, .art canvas:active {
        cursor: url("img/cursor/cursor-press.png") 1 1, pointer;
        cursor: image-set(url("img/cursor/cursor-press.png") 1x, url("img/cursor/cursor-press@2x.png") 2x) 1 1, pointer;
    }
}

/* Dithered underline: at rest a quiet dotted line marks every link; on hover it fills in,
   sparse pixels, then denser, then a solid line. */
a {
    padding-bottom: 1px;
    background-image: repeating-linear-gradient(90deg, color-mix(in srgb, currentColor 45%, transparent) 0 1px, transparent 1px 3px);
    background-repeat: no-repeat;
    background-position: 0 100%;
    background-size: 100% 1px;
}

a:hover {
    animation: dither-underline 0.24s forwards;
}

@keyframes dither-underline {
    0%, 32% { background-image: repeating-linear-gradient(90deg, currentColor 0 1px, transparent 1px 4px); }
    33%, 65% { background-image: repeating-linear-gradient(90deg, currentColor 0 1px, transparent 1px 2px); }
    66%, 100% { background-image: linear-gradient(currentColor, currentColor); }
}

main {
    max-width: 640px;
    padding: 2rem;
    display: flex;
    flex-direction: column;
    gap: 4rem;
}

@view-transition {
    navigation: auto;
}

h1 { view-transition-name: name; }
.switch { view-transition-name: switch; }
.photo { view-transition-name: photo; }

::view-transition-group(*) {
    animation-duration: 0.4s;
    animation-timing-function: ease;
}

section {
    display: grid;
    grid-template-columns: 1fr 3fr;
    gap: 1.5rem;
}

h1, h2 {
    font-size: inherit;
    font-weight: 500;
}

h2, .muted {
    font-weight: 400;
    opacity: var(--muted);
}

ul {
    list-style: none;
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
}

b {
    font-weight: 500;
}

.bitmap-mark {
    display: block;
    width: 128px;
    height: 128px;
    color: var(--fg);
    image-rendering: pixelated;
    view-transition-name: mark;
}

/* bitmap.js morphs the mark itself; keep the page transition off it. */
::view-transition-old(mark) {
    display: none;
}

::view-transition-new(mark) {
    animation: none;
}

/* Segmented toggles: the version switch (Инженерия · Дизайн) and the language (RU · EN).
   A thin frame with the current option filled reads as "pick one of these" at a glance. */
.switch,
.langs {
    display: inline-flex;
    border: 1px solid color-mix(in srgb, var(--fg) 28%, transparent);
}

.switch {
    margin-top: 1rem;
}

.switch > *,
.langs > * {
    padding: 0.2rem 0.65rem;
    background-image: none;
    transition: background-color 0.2s ease, opacity 0.2s ease;
}

.switch [aria-current],
.langs [aria-current] {
    background: var(--fg);
    color: var(--bg);
}

.switch a,
.langs a {
    opacity: var(--muted);
}

.switch a:hover,
.langs a:hover {
    opacity: 1;
    background-color: color-mix(in srgb, var(--fg) 8%, transparent);
    animation: none;
}

/* Language sits in the top-right corner, where visitors look for it; out of the flow, so the
   page grid is untouched. */
.langs {
    position: absolute;
    top: 2rem;
    right: 2rem;
    view-transition-name: langs;
}

/* Easter egg at the very bottom: a full-bleed band of dithered figures (art.js). */
.art {
    position: relative;
    height: max(320px, 60vh);
    margin-top: 2rem;
}

.art canvas {
    display: block;
    width: 100%;
    height: 100%;
    color: var(--fg);
    image-rendering: pixelated;
}

.photo {
    display: block;
    width: 100%;
    height: auto;
    margin-top: 2rem;
}

/* Contacts: muted label column, handle or number on the right. */
.contacts {
    gap: 0.5rem;
}

.contacts li {
    display: grid;
    grid-template-columns: 5.5rem 1fr;
    align-items: baseline;
}

.contacts a {
    justify-self: start;
}

.row {
    display: flex;
    justify-content: space-between;
    gap: 1rem;
}

.row > :last-child {
    flex-shrink: 0;
}

.cases {
    display: flex;
    flex-direction: column;
    gap: 3rem;
}

.case > p + p {
    margin-top: 0.5rem;
}

.grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 0.5rem;
    margin-top: 1.25rem;
}

.grid img,
.grid video {
    display: block;
    width: 100%;
    height: auto;
}

/* Images ignore the pointer so browser image overlays (Yandex) never attach;
   lightbox.js hit-tests clicks against the grid instead. */
.grid {
    cursor: zoom-in;
}

.grid img,
.grid video,
.photo {
    pointer-events: none;
    -webkit-touch-callout: none;
}

.grid img:focus-visible,
.grid video:focus-visible {
    outline: 1px solid var(--fg);
    outline-offset: 2px;
}

.grid .tall {
    grid-row: span 2;
    height: 0;
    min-height: 100%;
    object-fit: cover;
}

.grid .wide {
    grid-column: span 2;
}

/* Preloader: everything but the header mark stays laid out but hidden until preloader.js
   lets it in. The delayed animation reveals the page anyway if the script never runs. */
html.preload main section:not(:first-of-type),
html.preload main section:first-of-type > div,
html.preload .langs {
    opacity: 0;
    animation: preload-fallback 0.01s 5s forwards;
}

@keyframes preload-fallback {
    to { opacity: 1; }
}

main section:not(:first-of-type),
main section:first-of-type > div,
.langs {
    transition: opacity 0.5s ease;
}

.preloader-count {
    position: fixed;
    z-index: 200;
    font-weight: 500;
    pointer-events: none;
    opacity: 0;
    transition: opacity 0.3s ease;
}

.preloader-count.preloader-ready {
    opacity: 1;
}

.preloader-count.preloader-out {
    opacity: 0;
}

/* Lightbox */
html.lb-lock {
    overflow: hidden;
}

.lb {
    position: fixed;
    inset: 0;
    z-index: 100;
    display: grid;
    grid-template-rows: auto minmax(0, 1fr) auto;
    background: var(--bg);
    opacity: 0;
    visibility: hidden;
    transition: opacity 0.3s ease, visibility 0.3s;
}

.lb.lb-open {
    opacity: 1;
    visibility: visible;
}

.lb button {
    font: inherit;
    color: inherit;
    background: none;
    border: 0;
    padding: 0;
    opacity: var(--muted);
    outline: none;
    transition: opacity 0.2s ease;
}

.lb button:hover,
.lb button:focus-visible {
    opacity: 1;
}

.lb-close {
    justify-self: end;
    margin: 2rem 2rem 0;
}

.lb-frame {
    display: flex;
    min-height: 0;
    padding: 2rem;
    touch-action: none;
}

/* Clips the zoomed image inside the frame's padding, so it never touches the controls. */
.lb-stage {
    position: relative;
    flex: 1;
    min-width: 0;
    min-height: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
}

.lb-img {
    display: block;
    max-width: 100%;
    max-height: 100%;
    object-fit: contain;
    pointer-events: none;
    user-select: none;
    -webkit-user-drag: none;
    -webkit-touch-callout: none;
    transition: transform 0.35s cubic-bezier(.2, .8, .2, 1);
}

.lb-video {
    display: block;
    max-width: 100%;
    max-height: 100%;
    pointer-events: none;
}

.lb-is-video .lb-img,
.lb:not(.lb-is-video) .lb-video {
    display: none;
}

.lb-over {
    cursor: zoom-in;
}

.lb-zoomed .lb-frame {
    cursor: zoom-out;
}

.lb-zoomed .lb-img {
    transform: scale(2.5);
}

.lb-map {
    position: absolute;
    right: 1rem;
    bottom: 1rem;
    overflow: hidden;
    outline: 6px solid var(--bg);
    pointer-events: none;
    opacity: 0;
    transition: opacity 0.25s ease;
}

.lb-zoomed .lb-map {
    opacity: 1;
}

.lb-map img {
    display: block;
    max-width: 96px;
    max-height: 96px;
}

.lb-map i {
    position: absolute;
    border: 1px solid var(--fg);
    box-shadow: 0 0 0 100vmax rgb(237 242 244 / 0.6);
}

.lb-bitmap.lb-zoomed .lb-img {
    image-rendering: pixelated;
}

.lb-bar {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 1rem;
    padding: 0 2rem 2rem;
}

.lb-nav {
    display: flex;
    gap: 1rem;
    flex-shrink: 0;
}

.lb-single .lb-nav {
    display: none;
}

@media (prefers-reduced-motion: reduce) {
    @view-transition { navigation: none; }
}

@media (max-width: 600px) {
    main {
        padding: 2rem 16px;
        gap: 3rem;
    }

    section {
        grid-template-columns: 1fr;
        gap: 0.5rem;
    }

    .photo {
        margin-top: 1.5rem;
    }

    .bitmap-mark {
        width: 64px;
        height: 64px;
        margin-bottom: 1rem;
    }

    .langs {
        right: 16px;
    }

    .lb-close {
        margin: 1.5rem 16px 0;
    }

    .lb-frame {
        padding: 1.25rem 16px;
    }

    .lb-map {
        right: 0.75rem;
        bottom: 0.75rem;
    }

    .lb-map img {
        max-width: 64px;
        max-height: 64px;
    }

    .lb-bar {
        flex-direction: column;
        gap: 0.75rem;
        padding: 0 16px 1.5rem;
    }
}

/* Pixel magnifier cursors; last in the file so they win over the native zoom fallbacks. */
@media (hover: hover) and (pointer: fine) {
    .grid,
    .lb-over {
        cursor: url("img/cursor/cursor-zoom-in.png") 7 7, zoom-in;
        cursor: image-set(url("img/cursor/cursor-zoom-in.png") 1x, url("img/cursor/cursor-zoom-in@2x.png") 2x) 7 7, zoom-in;
    }

    .lb-zoomed .lb-frame {
        cursor: url("img/cursor/cursor-zoom-out.png") 7 7, zoom-out;
        cursor: image-set(url("img/cursor/cursor-zoom-out.png") 1x, url("img/cursor/cursor-zoom-out@2x.png") 2x) 7 7, zoom-out;
    }
}

/* Sits right under the contacts, closer than a regular section. */
.updated {
    margin-top: -2.5rem;
    align-items: end;
}

/* critter.js: a 12×12 canvas, one pixel per cell, sitting right before the date. The bird is
   a link to the Telegram channel, so it drops the dithered underline. */
.updated > span {
    display: flex;
    justify-content: flex-end;
}

.bird,
.bird:hover {
    display: block;
    padding: 0;
    background: none;
    animation: none;
}

.critter {
    display: block;
    width: 24px;
    height: 24px;
    margin-bottom: 3px;
    image-rendering: pixelated;
    opacity: var(--muted);
}

@media (max-width: 600px) {
    .updated {
        grid-template-columns: auto 1fr;
    }
}

/* Print / "Save as PDF": a plain one-to-two page résumé. Decoration goes, text stays, and
   external links print their address. */
@media print {
    @page {
        margin: 14mm 16mm;
    }

    html {
        font-size: 9.5pt;
    }

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

    main {
        max-width: none;
        padding: 0;
        gap: 1.1rem;
    }

    section {
        grid-template-columns: 1fr 4fr;
        gap: 1rem;
        break-inside: avoid;
    }

    ul {
        gap: 0.35rem;
    }

    li {
        break-inside: avoid;
    }

    .bitmap-mark,
    .bird,
    .switch,
    .langs,
    .photo,
    .grid,
    .art,
    .lb,
    .preloader-count {
        display: none !important;
    }

    html.preload main section:not(:first-of-type),
    html.preload main section:first-of-type > div {
        opacity: 1 !important;
        animation: none !important;
    }

    a {
        background: none !important;
        animation: none !important;
        padding: 0;
    }

    a[href^="http"]::after {
        content: " · " attr(href);
        opacity: 0.6;
    }

    /* Links within the site print as a full address too. */
    a[href^="/"]::after {
        content: " · mslv.space" attr(href);
        opacity: 0.6;
    }

    /* The MAX link is a long opaque token; the label already says where it goes. */
    a[href*="max.ru"]::after {
        content: none;
    }

    .updated {
        margin-top: 0;
    }

    /* The mark is hidden, so keep the name in the content column. */
    main section:first-of-type > div {
        grid-column: 2;
    }

    .cases {
        gap: 0.7rem;
    }

    .case > p + p {
        margin-top: 0.2rem;
    }

    /* Tighter rhythm so the design version, the longer one, fits a single page too. */
    main {
        gap: 0.85rem;
    }

    html:has(.cases) {
        font-size: 9pt;
    }

    html:has(.cases) ul {
        gap: 0.2rem;
    }
}
