@font-face {
  font-family: 'HelveticaNeueCyr';
  src: url('../fonts/HelveticaNeueCyr-Roman.ttf') format('truetype');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'HelveticaNeueCyr';
  src: url('../fonts/HelveticaNeueCyr-Medium.ttf') format('truetype');
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}

/* Type standard: every small (12px) uppercase tracked label on this
   site — nav, lang-switch, footer, case labels/years/links, CV tag,
   "все проекты"/"остальные проекты" etc. — uses letter-spacing:0.04em.
   This got out of sync a few times (0.01/0.06/0.1em creeping in on
   individual elements); 0.04em is the one value to use for this
   category going forward, not a per-element judgment call. */

:root {
  --black: #000000;
  --white: #ffffff;
  --gray-mid: #8a8a8a;
  --gray-line: #e2e2e2;
  --accent: #44ccff;
  --accent-light: #99eeff;
  --container: 1600px;
  --gap: 24px;
}

* { box-sizing: border-box; }

/* scrollbar-gutter reserves the scrollbar's width on every page, scrollable
   or not — without it, short pages (e.g. Contact) render slightly wider
   than tall ones, and .header-align's calc(...% ) position (it's centered
   against viewport width, not .wrap) visibly shifts right on navigation. */
/* -webkit-text-size-adjust:100% stops iOS Safari's automatic text-inflation
   (it boosts font sizes it judges "too small to read" on real devices only
   — never reproducible in desktop/emulated testing), which can silently
   grow specific text blocks and throw off spacing that measures correctly
   everywhere else. */
html { scroll-behavior: smooth; overflow-x: clip; scrollbar-gutter: stable; -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }

body {
  margin: 0;
  overflow-x: clip;
  background: var(--white);
  color: var(--black);
  font-family: 'HelveticaNeueCyr', 'Helvetica Neue', Helvetica, Arial, sans-serif;
  font-weight: 400;
  font-size: 14px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

img { max-width: 100%; display: block; }

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

.wrap {
  max-width: var(--container);
  margin: 0 auto;
  padding: 0 24px;
}

/* ---------- Header ---------- */

.site-header {
  position: sticky;
  top: 0;
  z-index: 50;
}
/* The header's own backdrop extends down to hold the Проекты dropdown,
   instead of the dropdown carrying a separate blurred plate of its own
   (tried first — looked like a floating card bolted onto the header
   rather than one surface). One ::before, not .site-header's own
   background plus a second stacked ::after (tried second) — two
   adjacent independently-blurred boxes showed a visible seam at the
   join even once their background/blur values matched, since each
   samples backdrop pixels only from within its own box. A single
   pseudo-element whose height just grows — 100% (matching whatever
   .site-header's real height is, tracked automatically since that IS
   this element's containing block) to 100% + room for the dropdown —
   is one continuous surface with no join to show a seam at.
   position:absolute + height on .site-header itself (not .site-header)
   is still the reason this isn't simply a taller .site-header: sticky
   or not, it sits in normal flow, so a real height change would push
   the page content below it down and back up on every hover — a jump
   nothing here calls for. z-index -1 for the same reason as before:
   .nav-dropdown, nested in .site-header__inner, needs to paint above
   this, not have this pseudo-element (generated last) paint over it. */
.site-header::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 100%;
  z-index: -1;
  background: rgba(255, 255, 255, 0.97);
  backdrop-filter: blur(12px);
  transition: height 0.25s ease;
}
.site-header:has(.nav-item:hover)::before,
.site-header:has(.nav-item:focus-within)::before {
  /* --dropdown-extra is measured live, in whatever browser is actually
     rendering the page (main.js) — a hardcoded px value here landed
     right in this dev browser and wrong on real Safari more than once
     (font-leading math that doesn't reproduce identically), and 39px
     is a fallback for the instant before that script runs, not a
     number this depends on. */
  height: calc(100% + var(--dropdown-extra, 39px));
}

.site-header__inner {
  position: relative;
  max-width: var(--container);
  margin: 0 auto;
  padding: 18px 24px;
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: 40px;
}

.logo { display: block; line-height: 0; }
.logo img { height: 13px; width: auto; display: block; }

.main-nav {
  display: flex;
  gap: 32px;
  position: relative;
  top: 2px;
}

.main-nav a {
  position: relative;
  font-size: 13px;
  font-weight: 400;
  line-height: 1.2;
  /* Matches .lang-switch button's tracking (same 12px uppercase size,
     same header row) — 0.06em looked noticeably wider side by side,
     especially now the labels are Cyrillic. */
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--black);
  transition: opacity 0.2s ease;
}
.main-nav a::after {
  content: "";
  position: absolute;
  top: -8px;
  bottom: -8px;
  left: -4px;
  right: -4px;
}
.main-nav a.active {
  text-decoration: underline;
  text-underline-offset: 4px;
}

/* Проекты's hover dropdown — Брендинг / Коммуникации, the same two
   sub-routes the section-tabs already write via pushState (see main.js
   and vercel.json's rewrites). padding-top on .nav-dropdown (not
   margin) is the hover bridge: the gap between the link and the panel
   stays part of the hoverable area, so the mouse doesn't lose :hover
   crossing it. :focus-within covers keyboard/tab navigation the same
   way :hover covers mouse.
   Root cause (found by comparing computed styles, not guessed): a flex
   item is automatically "blockified" per spec, so Обо мне/Контакты —
   direct <a> children of .main-nav — render as their full line-height
   box. Проекты, nested one level down inside .nav-item, stays a plain
   inline element instead, and getBoundingClientRect() on inline content
   returns the tight glyph run (~12px here), not the line-height box
   (15.6px) — hence it visibly sat lower despite identical computed
   font-size/line-height. (Two earlier attempts — display:contents on
   .nav-item, then display:flex, then matching its font metrics — never
   addressed this; they were all fixing .nav-item's own box, when the
   mismatch was actually the <a>'s inline-vs-blockified display type.)
   Forcing the link itself to display:block makes it use the same box
   model as its blockified siblings, in any browser, regardless of what
   .nav-item around it does. */
.nav-item { position: relative; }
.nav-item a { display: block; }
/* left:0 always (no narrow-screen right:0 flip, tried once) — Проекты
   is the first/leftmost nav item now, so there's always room to its
   right for this panel regardless of viewport width; a right-edge flip
   only made sense back when it could sit mid-row, and by then it was
   actively wrong: it right-aligned a panel wider than the link itself,
   which overhung past Проекты's own left edge instead of under it. */
/* padding-top 16px, matching .site-header__inner's own top padding
   above Проекты — top:100% here lands flush with .nav-item's bottom
   edge with no implicit gap of its own (a plain arithmetic fact of
   percentage-`top`, not a font-leading effect — an earlier version of
   this comment assumed leading was doing the work and was wrong).
   main.js measures this real rendered gap at runtime and mirrors it
   as .site-header::before's --dropdown-extra, so whatever this number
   actually renders as in a given browser, Коммуникации ends up the
   same distance above the extended backdrop's own edge — no matching
   number needs picking by hand on that end. */
.nav-dropdown {
  position: absolute;
  top: 100%;
  left: 0;
  padding: 16px 4px 0 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
  opacity: 0;
  pointer-events: none;
  transform: translateY(-6px);
  transition: opacity 0.2s ease, transform 0.2s ease;
}
.nav-item:hover .nav-dropdown,
.nav-item:focus-within .nav-dropdown {
  opacity: 1;
  pointer-events: auto;
  transform: translateY(0);
}
.nav-dropdown a {
  font-size: 13px;
  font-weight: 400;
  line-height: 1.2;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--gray-mid);
  white-space: nowrap;
}
.nav-dropdown a:hover { color: var(--black); }

.header-mark {
  display: block;
  line-height: 0;
  background: none;
  border: none;
  padding: 8px;
  margin: -8px -8px -8px auto;
  cursor: pointer;
}
.header-mark img { height: 14px; width: auto; display: block; pointer-events: none; }

/* Pins .lang-switch (RU/ENG), from 901px up, to the SAME column-2 start
   line used by every two-column content grid on the site (.home-grid,
   .about-journey, the page statement paragraph) — same 45fr/55fr/96px
   math as those grids, folded into one constant: column 2 starts at
   24px (left inset) + 45% of (contentWidth - 96px gap) + 96px, where
   contentWidth = min(1600px container, 100% of .site-header__inner)
   - 48px (its own 24px+24px side inset) = 55.2px + 0.45 * min(1600px,
   100%). .main-nav sits in normal flex flow right after the logo
   instead (left column), not grouped with the lang-switch anymore.
   Below 901px .header-align goes back to normal flex flow, pushed to
   the far right via margin-left:auto, with .lang-switch hidden in
   favor of .lang-switch--mobile. */
.header-align {
  display: flex;
  align-items: center;
}
@media (min-width: 901px) {
  .header-align {
    position: absolute;
    left: calc(55.2px + 0.45 * min(1600px, 100%));
    top: 50%;
    transform: translateY(calc(-50% + 2px));
  }
}

.lang-switch {
  display: flex;
  align-items: center;
  gap: 8px;
  position: relative;
  top: 1px;
}
.lang-switch button {
  font-family: inherit;
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  font-size: 13px;
  line-height: 1.2;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
/* Color + underline only, not font-weight — a heavier weight on click
   subtly shifted the button's rendered line box on mobile Safari
   (font metrics differ per weight), making it look like it dropped out
   of line with its sibling. Matches .main-nav a.active and
   .case__lang button.active, which use the same underline-only pattern. */
.lang-switch button { font-weight: 400; color: var(--gray-mid); }
.lang-switch button.active {
  color: var(--black);
  text-decoration: underline;
  text-underline-offset: 3px;
}
.lang-switch span {
  display: block;
  width: 1px;
  height: 9px;
  background: var(--black);
}

/* ---------- Mobile / tablet nav ----------
   Below 900px (phones through iPad-portrait widths) there's no room for
   the header-mark icon to double as a meaningful hamburger toggle, and
   a collapsible menu just adds a tap most visitors don't need for a
   3-link nav — so instead the nav stays permanently visible, pinned to
   the right of the logo. The lang-switch (normally an absolutely
   positioned overlay next to the desktop nav) moves out of the header
   entirely into its own right-aligned row at the top of the page
   content — see .lang-switch--mobile, added once per page right after
   <main class="wrap">. */
@media (max-width: 900px) {
  .header-mark { display: none; }
  .main-nav { gap: 20px; margin-left: auto; }
  /* Hide the whole (now-empty) container, not just .lang-switch inside
     it — an empty but still-flex .header-align was a zero-width flex
     item in .site-header__inner, so it still claimed a trailing 40px
     gap after .main-nav, keeping the nav from actually reaching the
     right edge. */
  .header-align { display: none; }
  .lang-switch--mobile {
    display: flex;
    margin: 20px 0 0 auto;
  }
}
@media (min-width: 901px) {
  .lang-switch--mobile { display: none; }
}

@media (max-width: 640px) {
  .wrap { padding: 0 16px; }
  .site-header__inner { padding: 14px 16px; gap: 16px; }
}

/* ---------- Hero ---------- */

.hero {
  padding: 56px 0 0;
}

.hero h1 {
  font-size: clamp(36px, 5vw, 52px);
  font-weight: 400;
  letter-spacing: -0.01em;
  margin: 0 0 14px;
}

.hero__row {
  display: grid;
  grid-template-columns: 45fr 55fr;
  gap: 32px 96px;
  align-items: center;
  margin-top: 22px;
}

.hero__tags {
  display: flex;
  flex-direction: column;
  gap: 0;
  font-size: 13px;
  font-weight: 400;
  line-height: 1.2;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--gray-mid);
}
/* Pulls the tags up visually closer to the H1 without using
   .hero__row's own margin-top — that margin also positions .hero__grid
   below it, so changing it would shift the avatar/bio row too.
   position:relative keeps .hero__row's flow height/position (and so
   .hero__grid's position) untouched; only the tags' painted position
   moves. Desktop only — mobile's H1/tags spacing is a different,
   already-tight module (line-height:0.95, margin-bottom:0) and wasn't
   part of this request. */
@media (min-width: 721px) {
  .hero__tags {
    position: relative;
    top: -16px;
  }
}

.hero__contact {
  display: flex;
  gap: 18px;
  font-size: 12px;
  font-weight: 400;
  line-height: 1.2;
  color: var(--black);
}
.hero__contact a { border-bottom: 1px solid var(--gray-line); }
.hero__contact a:hover { border-color: var(--black); }

.hero__grid {
  display: grid;
  grid-template-columns: 45fr 55fr;
  gap: 32px 96px;
  align-items: center;
  margin-top: 24px;
  min-height: 141px;
}
/* Home's hero (title+row+grid) sits inside a <section class="hero">, then
   adds the БРЕНДИНГ/КОММУНИКАЦИИ tabs below it — content the other 3
   pages don't have, which pushes Home's line further down. Rather than
   squeeze Home (that crowded the avatar/bio against the tabs), give
   About/Projects/Contact this same amount of breathing room below their
   hero__grid instead, since it's a direct child of .page-hero only on
   those 3 pages (on Home it's nested one level deeper, inside .hero).
   Desktop only — on mobile every page already gets its own correct,
   independently-measured height from main.js, so this would just add an
   unwanted extra gap there. */
@media (min-width: 721px) {
  /* min-height, not padding-bottom: .hero__grid is box-sizing:border-box
     with its own min-height:141px, so on About (whose hero__left/right are
     both empty) padding-bottom just got absorbed inside that 141px floor
     instead of adding real extra space — the box never grew past 141px.
     Overriding min-height directly sidesteps that and works the same
     whether the grid's content is empty (About) or already ~141px
     (Contact/Projects' doodle image). */
  .page-hero > .hero__grid {
    min-height: 211px;
  }
  /* Projects now has real tabs below .hero__grid again too (moved back
     out of .hero__tags, next to the divider like Home's), so it no
     longer needs the 211px stand-in for that missing content — back to
     the same 141px baseline .hero__grid already uses on Home. */
  .page-hero--wide-gap > .hero__grid {
    min-height: 141px;
  }
  /* True centering can't give both pages' statement text the same first
     line: centering a 3-line block and a 2-line block against even the
     SAME box puts their top edges at different heights (a taller block
     needs a smaller top offset to stay centered) — that's the actual
     cause, not just a stray pixel value. A shared fixed offset from
     .hero__grid's top sidesteps it: both pages start at the exact same
     row regardless of their own line count. 38px is where Home's text
     already naturally centers today (a 141px box around its current
     2-line height) — Home moves to align-items:start too so this
     margin fully controls it there as well (its avatar fills the row's
     full 141px height either way, so start vs. center is a no-op for
     the avatar itself). */
  .hero > .hero__grid,
  .page-hero > .hero__grid {
    align-items: start;
  }
  .hero > .hero__grid .hero__bio--statement,
  .page-hero > .hero__grid .hero__bio--statement {
    margin-top: 38px;
  }
}

.hero__avatar {
  width: 141px;
  aspect-ratio: 1 / 1;
  overflow: hidden;
  background: #ffffff;
}
.hero__avatar img { width: 100%; height: 100%; object-fit: cover; display: block; }

.hero__links {
  display: flex;
  justify-content: space-between;
  gap: 28px;
}
.hero__links a {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
  font-weight: 400;
  line-height: 1.2;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--gray-mid);
  transition: color 0.15s ease;
}
.hero__links a::after {
  content: "";
  position: absolute;
  top: -8px;
  bottom: -8px;
  left: -4px;
  right: -4px;
}
.hero__links a:hover { color: var(--black); }
.hero__links img { width: 10px; height: 10px; }

.hero__bio {
  font-size: 13px;
  line-height: 1.4;
  color: var(--black);
  max-width: 640px;
  margin: 0;
}
.hero__bio--statement {
  font-size: clamp(20px, 2.6vw, 32px);
  font-weight: 400;
  line-height: 1.25;
  letter-spacing: -0.01em;
  max-width: 580px;
}

@media (max-width: 720px) {
  /* .page-title (About/Projects/Contact) never got this same tightening,
     so on mobile its box sat ~20px taller than Home's .hero h1 — pushing
     everything below it (tags, gallery, divider) down by that much and
     making Home/Projects visibly misaligned side by side. */
  .hero h1, .page-title { line-height: 0.95; margin-bottom: 0; }
  .hero__row { grid-template-columns: 1fr; gap: 24px; margin-top: 16px; }
  .hero__grid { grid-template-columns: 1fr; margin-top: 32px; }
  .hero__links { flex-wrap: wrap; justify-content: flex-start; gap: 0 24px; }
  /* .hero__grid's own box height (min-height:141px, set above) is what
     JS main.js measures to place .page-divider — that line's position
     must stay pixel-identical across every page (Home/About/Projects/
     Contact), so the grid's outer box is deliberately left untouched
     here. Projects' subtitle sat low within that box because
     align-items:center centered it in the leftover min-height space;
     align-items:start pins it right under hero__left instead, so the
     divider below doesn't move but the subtitle sits near the top of
     the gap instead of the middle. */
  .page-hero > .hero__grid { align-items: start; }
  .page-hero > .hero__grid .hero__bio--statement { margin-top: -33px; }
  /* Home's own H1-to-descriptor gap ("Владислав Мордвин" down to "Art
     Direction / Visual Communication") was 16px, but Projects' H1 sits
     32px above its own .hero__tags content (the БРЕНДИНГ/КОММУНИКАЦИИ
     tabs, in that same structural slot) — flipping between the two
     pages made an identical position read as two different heights.
     Matches Projects' 32px here instead of touching the tabs there. */
  .page-hero--tight-gap .hero__row { margin-top: 32px; }
}

.section-tabs {
  display: flex;
  gap: 48px;
  margin-top: 56px;
}

.section-tab {
  position: relative;
  font-family: inherit;
  font-size: 13px;
  font-weight: 400;
  line-height: 1.2;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  background: none;
  border: none;
  padding: 0;
  color: var(--gray-mid);
  cursor: pointer;
}
.section-tab::after {
  content: "";
  position: absolute;
  top: -8px;
  bottom: -8px;
  left: -4px;
  right: -4px;
}

.section-tab.active {
  color: var(--black);
  text-decoration: underline;
  text-underline-offset: 4px;
}

@media (max-width: 720px) {
  .section-tabs { margin-top: 32px; gap: 28px; }
}

/* ---------- Home project grid ---------- */

/*
 * TWO-TRACK PROJECT GRID — the reusable template for this site.
 *
 * Desktop = exactly two container types, side by side:
 *   left track  (narrower, ~45%) and right track (~55% wide).
 * Every project sits in one cell. Cells are placed in normal reading
 * order (left, right, left, right, ...) so row 1 = [project A | project B],
 * row 2 = [project C | project D], etc.
 *
 * The height-matching rule ("left and right in the same row must be
 * EXACTLY the same height") is enforced by CSS Grid itself, not by
 * copying pixel numbers: a grid row's height is always the tallest
 * cell in that row, and align-items:stretch (the Grid default) forces
 * the shorter cell to match it. So you never hand-tune heights —
 * just add a new .proj-block in the right spot in the DOM and the
 * grid keeps both columns level automatically.
 *
 * Inside a cell: .proj-tiles is a small sub-grid (1-4 images) that
 * always fills 100% of the cell's stretched height via object-fit:
 * cover — one hero image is just a 1-tile version of the same thing.
 * That's the only pattern to learn to add a new project.
 */

.home-grid {
  display: grid;
  grid-template-columns: 45fr 55fr;
  gap: 80px 96px;
  align-items: stretch;
  margin-top: 32px;
}

.proj-block {
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
}

.proj-tiles {
  display: grid;
  grid-template-columns: 1fr 1fr;
  grid-auto-rows: 1fr;
  gap: 13px;
  flex: 1 1 auto;
  min-width: 0;
  min-height: 0;
}
.proj-tiles.tiles-1 { grid-template-columns: 1fr; }
.proj-tiles .tile { display: block; min-width: 0; min-height: 0; }
.proj-tiles .tile > img,
.proj-tiles .tile > .tile-stack {
  width: 100%;
  height: 100%;
}
.proj-tiles img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  background: #f2f2f2;
}
.proj-tiles .span-2 { grid-column: span 2; }
.proj-tiles .span-2-rows { grid-row: span 2; }
.tile-stack {
  display: flex;
  flex-direction: column;
  gap: 13px;
}
.tile-stack img { flex: 1 1 0; min-height: 0; object-fit: cover; }

/* .composite — for the handful of blocks pulled 1:1 from a specific
   Figma frame (exact left/top/width/height % + a background photo
   bleeding through the gaps), instead of the generic .proj-tiles grid.
   Children are positioned with inline style percentages copied
   straight from Figma's node coordinates — see README.md. */
.composite {
  position: relative;
  overflow: hidden;
  flex: 1 1 auto;
  min-height: 0;
}
.composite img,
.composite .cbox {
  position: absolute;
  display: block;
}
.composite > img { object-fit: cover; }
.composite .cbox { overflow: hidden; }
.composite .cbox img { max-width: none; object-fit: cover; }

.proj-block__cap {
  font-size: 13px;
  font-weight: 400;
  line-height: 1.2;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  display: flex;
  justify-content: space-between;
  padding-top: 4px;
  margin-top: 6px;
}
.proj-block__cap span:last-child { color: var(--gray-mid); font-weight: 400; }

/* Коммуникации tiles that reveal a description on click — same
   collapsed-by-default accordion pattern as .exp-item__toggle/__panel
   and .case__role-toggle/__panel, just wrapping the whole tile
   (image + caption) as the clickable button instead of a text line. */
.proj-block__toggle {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-width: 0;
  min-height: 0;
  width: 100%;
  background: none;
  border: none;
  padding: 0;
  margin: 0;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
}
.proj-block__cap-title {
  display: inline-flex;
  align-items: baseline;
  gap: 8px;
}
.proj-block__arrow {
  flex: none;
  width: 7px;
  height: 7px;
  margin-top: 3px;
  border-right: 1px solid var(--black);
  border-bottom: 1px solid var(--black);
  transform: rotate(-45deg);
  transition: transform 0.2s ease;
}
.proj-block__toggle[aria-expanded="true"] .proj-block__arrow { transform: rotate(45deg); }
.proj-block__panel {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 0.25s ease;
}
.proj-block__panel.is-open { grid-template-rows: 1fr; }
.proj-block__panel-inner { overflow: hidden; min-height: 0; }
.proj-block__desc {
  font-size: 13px;
  line-height: 1.4;
  text-transform: none;
  letter-spacing: normal;
  color: var(--black);
  margin: 0;
  padding-top: 12px;
}

/* Large, symmetric top/bottom whitespace (moved off .home-grid's own
   padding-bottom, so it's controlled in one place) — this line reads
   as a closing statement for the grid above it, not just another row
   packed in at the standard 32px rhythm. */
.home-more {
  margin: 80px 0 0;
  text-align: center;
  font-size: 12px;
  font-weight: 400;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding-bottom: 32px;
}
.home-more a { display: inline-flex; align-items: baseline; gap: 8px; }
.home-more a:hover { opacity: 0.6; }
.home-more__arrow {
  flex: none;
  width: 7px;
  height: 7px;
  margin-top: 4px;
  border-right: 1px solid var(--black);
  border-bottom: 1px solid var(--black);
  transform: rotate(-45deg);
}

@media (max-width: 720px) {
  .home-grid { grid-template-columns: 1fr; }
  .proj-block { grid-column: 1 !important; }
  /* Mobile-only swap: Encore (order 0, its natural DOM position) is the
     second-to-last branding tile with Tak Development right after it —
     giving Encore order:1 alone is enough to drop it behind Tak
     Development, without touching every other tile's own default
     order:0. :not(.home-grid--comm) scopes this to the branding grid
     only — Encore's OTHER tile, in the Коммуникации grid, is a
     different <a> with the same href and must stay untouched. */
  .home-grid:not(.home-grid--comm) > a[href="/projects#encore"] { order: 1; }
}

/* Коммуникации tab — equal-width columns and a tighter gap than the
   Брендинг grid's 45fr/55fr + 96px (matches the Figma comp: two
   684px columns with a ~24px gap, now three like the 2026 draft — not
   the canonical asymmetric grid),
   since these cards are all single hero shots, not multi-tile
   composites. No case-page link yet, so .proj-block--static is a
   plain div, not an anchor — cursor stays default, no hover-link
   affordance for a destination that doesn't exist. */
.home-grid--comm { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 40px 24px; align-items: start; }
/* Cards without a subtitle (Купер, Мото 54) used to stretch their image
   to the row's height (.home-grid's align-items: stretch + .proj-tiles'
   flex: 1) — keep every cover at its own aspect-ratio instead. */
.home-grid--comm .proj-tiles { flex: none; }
/* 3 columns (as in the 2026 draft) get too narrow for the titles below
   ~900px — two there, one on phones (see the 720px rule below). */
@media (max-width: 900px) {
  .home-grid--comm { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
.proj-block--static { cursor: default; }
.home-more--note { text-align: center; }

/* Same specificity as .home-grid--comm above and later in the
   cascade, so without this override it would win over the earlier
   mobile .home-grid{grid-template-columns:1fr} rule and force two
   half-width columns on mobile instead of one full-width stack. */
@media (max-width: 720px) {
  .home-grid--comm { grid-template-columns: 1fr; }
}

/* ---------- About page ---------- */

.page-title {
  font-size: clamp(36px, 5vw, 52px);
  font-weight: 400;
  letter-spacing: -0.01em;
  padding-top: 56px;
  margin: 0 0 14px;
}

/* .page-hero's height is set by JS (assets/js/main.js) to this page's own
   real content height + a fixed 20px — the same 20px as .home-grid's own
   margin-top below the line (see there), so the divider sits symmetrically:
   exactly 20px under whatever content precedes it, exactly 20px above the
   first project row, on every page and at any viewport width. A plain CSS
   px/vw guess can't track real text-wrapping (which varies by browser/
   font), so this is measured directly instead of estimated. The height
   below is just a pre-JS fallback (divider sits right after content). */
.page-hero {
  position: relative;
}
.page-hero .page-divider {
  position: absolute;
  left: 50%;
  right: 50%;
  bottom: 0;
  width: 100vw;
  margin-left: -50vw;
  border-bottom: 1px solid var(--black);
}

/* About's own reference gap (32px, same as "Опыт работы"'s two divider
   lines) for the whitespace around its first divider too. Keeping it
   scoped here leaves Contact unchanged. */
#cv-zone {
  --about-content-gap: 32px;
  --about-gallery-bottom-gap: 32px;
  --page-divider-gap: 32px;
}
#cv-zone .page-title { margin-bottom: 0; }

/* Projects: 32px gap from the tabs down to the divider — same value
   and same reasoning as Home's own --tight-gap just below (the tabs
   are the last element before the line here too now, so this is the
   same rule, not a separate "wide" one; the name's just stale). */
.page-hero--wide-gap { --page-divider-gap: 32px; }

/* Home: fixed 32px gap from the БРЕНДИНГ/КОММУНИКАЦИИ tabs down to
   this divider, same at every width — matches the reference 32px the
   line itself uses down to the project grid below it (.home-grid's
   own margin-top), so the line sits with equal space on both sides.
   This is JS-measured (see main.js's .page-hero height fit()), not a
   plain CSS margin, since the tabs' own height/position isn't
   otherwise fixed across pages. */
.page-hero--tight-gap { --page-divider-gap: 32px; }

/* 45fr/55fr, not an equal split — this is the site's one canonical
   two-column grid (same ratio as .home-grid and the header's own
   .header-align, which is what visibly pins the RU/ENG toggle in the
   header on every page), so column 2 lines up with RU/ENG and with
   every other two-column block on this page. */
.about-journey {
  display: grid;
  grid-template-columns: 45fr 55fr;
  gap: 32px 96px;
  margin-top: 32px;
  /* Row items default to stretch, so a short right-column entry
     (1-line "Бренд-дизайн") fills the same row height as a taller
     left-column one (3-line) and .journey-item's own align-items:center
     then centers its text within that borrowed height — pushing its
     first line down instead of matching the left column's first line.
     align-items:start keeps each item at its own natural height, so
     both columns' first lines land on the same row. */
  align-items: start;
}
/* align-items:start, not center — with center, whichever child (icon
   or date+text) is taller drives this item's own height, and the
   SHORTER child then gets centered within that borrowed height. Since
   the icon's margin box (see below) is taller than a short one-line
   entry's date+text ("Бренд-дизайн"), centering pushed that text down
   from the item's own top — breaking the cross-column first-line
   alignment .about-journey's align-items:start is supposed to
   guarantee (its own diff between icon and item height didn't matter
   for the auto-placement, but it did for this text's own position).
   start makes date+text ALWAYS begin at this item's own top, no matter
   which sibling is taller, restoring that guarantee unconditionally. */
.journey-item {
  display: grid;
  grid-template-columns: 56px 1fr;
  gap: 16px;
  /* align-items:start, not center: the text column must ALWAYS start
     at this item's own top, no matter how the icon compares to it —
     that's what keeps a paired row's two first lines level (see the
     .about-journey comment above). Only the icon itself (via its own
     align-self below) centers against whatever that row's height ends
     up being. */
  align-items: start;
}
.journey-item__icon {
  width: 56px;
  height: 56px;
  display: flex;
  align-items: center;
  justify-content: center;
  flex: none;
  /* Row height = max(icon, text). When text is taller (the normal
     case), this centers the icon against the text's full height. When
     the icon is taller than a short one-line entry ("Бренд-дизайн"),
     the row height equals the icon's own height, so this resolves to
     zero offset — icon and text both simply sit flush at the item's
     top instead of the icon overhanging above it. Either way the text
     itself never moves, so cross-column first-line alignment holds. */
  align-self: center;
}
.journey-item__icon img { width: 100%; height: 100%; object-fit: contain; display: block; }
.journey-item__years {
  display: block;
  font-size: 12px;
  font-weight: 400;
  color: var(--gray-mid);
  margin-bottom: 4px;
}
/* Exact same size/line-height/tracking as the hero "statement" lead
   text on Home (.hero__bio--statement, "6 лет создаю..."), not a fixed
   25px — that diverged from it on mobile (20px there vs 25px fixed
   here). */
.journey-item__text {
  font-size: clamp(20px, 2.6vw, 32px);
  font-weight: 400;
  line-height: 1.25;
  letter-spacing: -0.01em;
  margin: 0;
}

@media (max-width: 720px) {
  .about-journey { grid-template-columns: 1fr; gap: 32px; }
}

/* On desktop the grid places items row by row (1=top-left, 2=top-right,
   3=bottom-left, 4=bottom-right), so DOM order alone can't give both a
   chronological mobile stack AND a chronological desktop reading —
   mobile needs architecture/filming/brand-design/art-direction in that
   DOM order, but that puts filming (2020–2021) top-right and
   brand-design (2021–2024) bottom-left, i.e. each column no longer
   runs chronologically top-to-bottom. `order` swaps just the visual
   desktop position of those two, leaving the DOM (and mobile's source
   order) untouched. */
@media (min-width: 721px) {
  .about-journey .journey-item:nth-child(1) { order: 1; }
  .about-journey .journey-item:nth-child(2) { order: 3; }
  .about-journey .journey-item:nth-child(3) { order: 2; }
  .about-journey .journey-item:nth-child(4) { order: 4; }

  /* Desktop-only bump, mobile stays at 56px — align-items:center above
     needs no extra formula for the larger size, it centers against
     the text's own full height regardless of icon size. */
  .journey-item { grid-template-columns: 64px 1fr; }
  .journey-item__icon { width: 64px; height: 64px; }
}

.about-row {
  display: grid;
  grid-template-columns: 90px 1fr 1fr;
  gap: 32px;
  margin-top: 32px;
}
.about-row:last-of-type { padding-bottom: 32px; }

@media (max-width: 720px) {
  .about-row { grid-template-columns: 1fr; gap: 16px; }
}

/* Mobile: stack label/text/gallery in one flex column so the gallery
   row can move above the text (order:-1 below) without touching the
   source order or duplicating markup. Desktop switches this to a real
   two-column grid below — no separate label column: with it gone,
   "Скачать CV" spans its own row above the text instead, freeing the
   grid to be the site's actual 45fr/55fr two-column ratio (same as
   .home-grid/.header-align, which is what visibly pins the RU/ENG
   toggle in the header), rather than three cramped tracks. */
.about-bio-wrap { display: flex; flex-direction: column; }
.about-gallery-row {
  /* No margin-top here — .about-bio-wrap's own margin-top (below)
     already provides the divider→content gap; adding another one here
     (mobile only, since desktop already zeroes this out below) doubled
     it to 64px. */
  /* Images end at the exact edge of their box, unlike text headings,
     so this compensates for the missing line-box whitespace and yields
     the same rendered secondary gap below the carousel. */
  margin-bottom: var(--about-gallery-bottom-gap);
}

/* The first divider is positioned by main.js; this is its matching
   lower gap before the biography on every breakpoint — same 32px
   reference gap as the "Опыт работы" section around its own two
   divider lines, not the site's larger 56px section module. */
#cv-zone .about-bio-wrap { margin-top: 32px; }

@media (min-width: 721px) {
  .about-bio-wrap {
    display: grid;
    grid-template-columns: 45fr 55fr;
    gap: 16px 96px;
  }
  .about-gallery-row {
    grid-column: 2;
    grid-row: 1;
    align-self: center;
    margin-top: 0;
    min-width: 0;
  }
}

/* Full-bleed black line — same visual language as .page-divider/
   .footer-divider — marking the bio→"Опыт работы" transition. Same
   32px reference gap as above/below "Опыт работы" itself. */
.about-divider {
  position: relative;
  height: 1px;
  margin-top: 32px;
}
.about-divider::before {
  content: "";
  position: absolute;
  left: 50%;
  right: 50%;
  top: 0;
  width: 100vw;
  margin-left: -50vw;
  border-bottom: 1px solid var(--black);
}

/* This section's own reference gap: the same 32px used below, between
   the last work entry and the footer divider — applied here (top
   divider → heading) and on mobile (heading → heading) so every gap
   tied to this section's two bounding divider lines, or between its
   own headings, uses one consistent value instead of the site's
   larger 56px section module. */
#cv-zone .about-experience { margin-top: 32px; }

/* On mobile the two experience columns become consecutive sections.
   Separate the second one ("Фриланс") from the final work entry with
   this section's own reference gap. ("Скачать CV" now lives inside the
   first column, under its h2, so "Фриланс" is the 2nd child of
   .about-experience, not the 3rd.) */
@media (max-width: 720px) {
  /* .about-row's own 16px grid-gap (row gap, single column here) already
     runs between the two columns — without cancelling it, it stacks with
     this margin-top (16+32=48) instead of landing on the reference 32. */
  #cv-zone .about-experience { gap: 0; }
  #cv-zone .about-experience > :nth-child(2) {
    margin-top: 32px;
  }
}

.about-gallery-caption {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: 8px;
  font-size: 12px;
  font-weight: 400;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
.about-gallery-title {
  color: var(--gray-mid);
  border-bottom: none;
}
.about-gallery-title[aria-disabled="true"] {
  cursor: default;
  pointer-events: none;
}
.about-gallery-nav {
  display: flex;
  align-items: center;
  gap: 12px;
  color: var(--gray-mid);
  flex: none;
}

.about-gallery.case__gallery {
  width: 100%;
  aspect-ratio: 683 / 233;
  overflow-x: auto;
  overflow-y: hidden;
  touch-action: pan-x;
  -webkit-overflow-scrolling: touch;
  padding-bottom: 0;
  /* Custom .about-gallery-progress below replaces the native scrollbar
     as the "always visible, obviously scrollable" affordance — the
     native one (styled further up for the generic .case__gallery) is
     hidden here since OS-level auto-hide behavior can't be relied on. */
  scrollbar-width: none;
}
.about-gallery.case__gallery::-webkit-scrollbar { display: none; }
.about-gallery.case__gallery img {
  height: 100%;
  width: auto;
}

.about-gallery-progress {
  height: 3px;
  margin-top: 8px;
  background: var(--gray-line);
  position: relative;
  overflow: hidden;
}
.about-gallery-progress__thumb {
  position: absolute;
  top: 0;
  left: 0;
  height: 100%;
  background: var(--gray-mid);
}

/* Desktop only: bleed the gallery itself (images + progress bar, not
   the caption row above them) past .wrap's right padding to the true
   viewport edge. A negative margin-right would NOT do this — margins
   don't extend an element's own box, they only pull whatever comes
   after it — so this grows the element's own width past 100% instead,
   by exactly the gap between its column's right edge and the viewport
   edge: (100vw - wrapWidth)/2 + 24px of .wrap's own padding, where
   wrapWidth = min(1600px, 100vw) is .wrap's own actual width (capped
   by --container), so this holds at any viewport size instead of just
   under 1648px. */
@media (min-width: 721px) {
  .about-gallery.case__gallery,
  .about-gallery-progress {
    width: calc(100% + (100vw - min(1600px, 100vw)) / 2 + 24px);
  }
}

@media (max-width: 720px) {
  .about-gallery-row {
    order: -1;
    width: 100vw;
    margin-left: calc(50% - 50vw);
    margin-right: calc(50% - 50vw);
  }
  .about-gallery-caption { padding: 0 16px; }
  .about-gallery.case__gallery,
  .about-gallery-progress { width: auto; }
  .about-gallery.case__gallery { margin: 0; }
  .about-gallery-progress { margin: 8px 0 0; }
}

.about-body {
  display: grid;
  gap: 24px;
  max-width: 480px;
  margin: 0;
}
.about-body p {
  font-size: 15px;
  font-weight: 400;
  line-height: 1.4;
  margin: 0;
}
.brand-name {
  text-decoration: underline;
  text-underline-offset: 3px;
}

/* Same reasoning as .about-bio-wrap above: drop the separate 90px
   label column — "Скачать CV" spans its own row instead — so this
   becomes a real 45fr/55fr two-column grid (matching .about-journey/
   .home-grid/.header-align) instead of three cramped tracks. */
@media (min-width: 721px) {
  .about-experience { grid-template-columns: 45fr 55fr; gap: 16px 96px; }
}

/* Same size/weight/tracking category as the page H1 (.page-title) —
   "Опыт работы" / "Фриланс" read as headings, not sub-headings. */
.about-experience h2 {
  font-size: clamp(36px, 5vw, 52px);
  font-weight: 400;
  letter-spacing: -0.01em;
  margin: 0 0 32px;
}
.exp-item + .exp-item { margin-top: 0; }

/* Collapsed by default — only the role/company/dates line (black) shows
   until the visitor opens it; the link + description (gray) live inside
   .exp-item__panel, height-animated via JS setting an explicit
   max-height (see main.js), since CSS can't transition to/from "auto". */
.exp-item__toggle {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  width: 100%;
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
}
.exp-item__role {
  font-size: 15px;
  font-weight: 400;
  line-height: 1.4;
}
.exp-item__place {
  text-decoration: underline;
  text-underline-offset: 3px;
}
.exp-item__arrow {
  flex: none;
  width: 7px;
  height: 7px;
  margin-top: 4px;
  border-right: 1px solid var(--black);
  border-bottom: 1px solid var(--black);
  transform: rotate(-45deg);
  transition: transform 0.2s ease;
}
.exp-item__toggle[aria-expanded="true"] .exp-item__arrow { transform: rotate(45deg); }

/* grid-template-rows 0fr->1fr, not max-height — animating max-height
   (or height) forces layout recalculation every frame; this variant
   only needs the row track size, which the compositor can handle much
   more cheaply, and needs no JS scrollHeight measurement either. */
.exp-item__panel {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 0.25s ease;
}
.exp-item__panel.is-open { grid-template-rows: 1fr; }
.exp-item__panel-inner {
  overflow: hidden;
  min-height: 0;
  padding-top: 8px;
}
.exp-item__link {
  display: inline-block;
  font-size: 15px;
  font-weight: 400;
  color: var(--black);
  margin-bottom: 8px;
}
.exp-item__link:hover { opacity: 0.6; }
.exp-item__desc {
  font-size: 15px;
  line-height: 1.4;
  color: var(--gray-mid);
  margin: 0 0 24px;
}

/* ---------- Contact page ---------- */

/* Canonical 45fr/55fr two-column grid (same ratio as .home-grid/
   .about-journey/.header-align, which is what visibly pins the RU/ENG
   toggle in the header) — list on the left, email/phone on the right. */
.contact-row {
  display: grid;
  grid-template-columns: 45fr 55fr;
  gap: 32px 96px;
  margin-top: 32px;
}

/* One tight block with a single shared line-height — like a paragraph
   wrapping onto multiple lines — instead of each entry padded out into
   its own separate row. */
.contact-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
}
/* Same size/weight/line-height/tracking category as the hero
   "statement" lead text on Home (.hero__bio--statement, "6 лет создаю
   дизайн-системы...") — same category as .contact-statement on the
   right, so both columns read as one consistent type scale instead of
   the list towering over the email/phone block next to it. */
.contact-list a,
.contact-list span {
  display: block;
  font-size: clamp(20px, 2.6vw, 32px);
  font-weight: 400;
  line-height: 1.25;
  letter-spacing: -0.01em;
}
.contact-list span { color: var(--gray-mid); cursor: default; }
.contact-list a { transition: opacity 0.15s ease; }
.contact-list a:hover { opacity: 0.55; }

/* Same size/weight/line-height/tracking category as the hero
   "statement" lead text on Home (.hero__bio--statement, "6 лет создаю
   дизайн-системы..."), reused here for the email/phone block. */
.contact-statement {
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.contact-statement a {
  font-size: clamp(20px, 2.6vw, 32px);
  font-weight: 400;
  line-height: 1.25;
  letter-spacing: -0.01em;
  color: var(--black);
  transition: opacity 0.15s ease;
}
.contact-statement a:hover { opacity: 0.6; }

@media (max-width: 720px) {
  /* Email is too long to share a row with the list at this width, so
     it drops below instead. */
  .contact-row { grid-template-columns: 1fr; gap: 0; }
  .contact-statement { margin-top: 24px; }
}

/* ---------- Projects page ---------- */

/* Asymmetric on purpose: 56px below (tier 2 gap, same module as
   About's section gap) keeps real breathing room between one case's
   gallery and the next case's divider, but that same 56px above felt
   too big for divider-to-content specifically — 30px matches
   .case__desc's own margin-bottom (content-to-gallery), so both gaps
   inside a single case read as the same size. */
.case {
  position: relative;
  padding: 32px 0 56px;
}
.case::before {
  content: "";
  position: absolute;
  top: 0;
  left: 50%;
  right: 50%;
  width: 100vw;
  margin-left: -50vw;
  border-top: 1px solid var(--black);
}
.case:first-of-type { margin-top: 0; }
.case:first-of-type::before { content: none; }

/* Removed from view everywhere — redundant with the header's own
   site-wide RU/ENG toggle, and the role button now takes its old
   90px-column spot instead (see .case__desc--split below). Still
   rendered (not removed from the DOM) so the shared lang-switch JS
   keeps a consistent element set to sync .active across, it just
   never takes up visible space. */
.case__lang {
  display: none;
  gap: 10px;
  font-size: 13px;
  font-weight: 400;
  letter-spacing: 0.04em;
}
.case__lang button {
  background: none;
  border: none;
  font: inherit;
  color: var(--gray-mid);
  cursor: pointer;
  padding: 0;
}
.case__lang button.active { color: var(--black); text-decoration: underline; text-underline-offset: 3px; }

/* Normal flow now at every width, not just mobile — used to float
   above the case's own divider line on desktop (position:absolute,
   top:-22px), matching case__year--inline below; both switched to the
   mobile-only convention that already existed (case's own 32px
   padding-top gives the line-to-title gap for free) at the user's
   request, so desktop reads the same as mobile instead of the two
   diverging. See .case__head's old mobile-only media query, now
   merged in here, and case__year--inline just below. */
.case__head {
  position: static;
  margin-bottom: 16px;
}
.case__title {
  font-size: 13px;
  font-weight: 400;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
/* Caps title width so a long one wraps instead of running into the
   site-link now sharing its row (.case__desc--split .case__link, See
   that rule) — a real collision at narrower desktop widths for cases
   with longer titles (confirmed: "Яндекс Маркет — активация «С Днём
   Курьера!»" actually overlapped market.yandex.ru at 721px). 45% + 24px
   leaves a comfortable ~29px gap before the link's own left:45% + 52.8px.
   Scoped to :not(.case__head--flow) — the minimal communications cases
   use a flex row instead (title/year, no link on that row at all), so
   they have no collision to guard against and more width to use; this
   would just wrap their titles early for no reason. */
@media (min-width: 721px) {
  .case__head:not(.case__head--flow) .case__title {
    max-width: calc(45% + 24px);
  }
}
/* Lives next to the role button (see .case__role-row) instead of
   inside .case__head — at mobile's title size a year can't reliably
   share the title's own row without wrapping, so it sits with the
   role row instead, on every card, not just the ones that would
   actually overflow (mixing "year up top" and "year down here"
   case-by-case reads as inconsistent). Desktop pulls it back up to
   the title's own -22px row below, since there it always has room. */
.case__year {
  flex: none;
  font-size: 13px;
  font-weight: 400;
  letter-spacing: 0.04em;
  color: var(--gray-mid);
}
.case__role-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
}
/* Same at every width now, not just mobile — .case__head sits in
   normal flow (see its own comment), starting right at .case's own
   32px padding-top, so the year lines up with that instead of being
   pulled up to float above the divider like it used to on desktop. */
.case__year--inline {
  position: absolute;
  top: 32px;
  right: 0;
}

/* Minimal communications cases (title + year + a single image, no
   .case__desc below) don't have that content to visually ground the
   -22px "float above your own divider" convention branding cases used
   to use via position:absolute — with nothing under it there,
   .case__head's own box was collapsing to zero height, which on
   mobile Safari specifically left title+year rendering off in the
   wrong place entirely (reported as "даты улетели"). This stays in
   normal flow instead — no position:absolute at all — so .case always
   has real, correctly-measured content driving its height. Branding
   cases now use the same flow convention (.case__head, above), so
   there's no longer a desktop-only pull-up to match here either —
   this is just the one shared convention at every width. */
.case__head--flow {
  position: static;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: 32px;
}
/* A site-link for a minimal communications case (no role/.case__desc at
   all yet) — same idea as .case__desc--split .case__link below (see its
   own comment), just without that grid to hang off of. Mobile: plain
   flow, its own row between the title and the gallery, 32px both sides
   to match the site's standard gap. Desktop: pulled onto the title/year
   row itself via the same position/top/left this other rule uses, so
   the two stay pixel-identical instead of two numbers that happen to
   match today. */
.case__head--flow ~ .case__link {
  margin-bottom: 32px;
}
@media (min-width: 721px) {
  .case__head--flow ~ .case__link {
    position: absolute;
    top: 32px;
    left: calc(45% + 52.8px);
    margin-bottom: 0;
  }
}

/* The project link used to sit in its own .case__meta row next to
   .case__lang; it now lives at the top of the description's right
   column (case__desc-col, or — for the single-block .case__desc
   cases — its own grid row) so that column's own right edge is what
   it aligns to, per the Пермь reference. width:fit-content lets
   margin-left:auto pull it there in both a plain block (.case__desc-col)
   and as a grid item (.case__desc) — a grid item stretches to fill its
   cell by default only when its width stays auto. */
.case__link {
  display: block;
  width: fit-content;
  margin-bottom: 12px;
  font-size: 13px;
  font-weight: 400;
  line-height: 1.4;
  color: var(--gray-mid);
}
.case__link:hover { color: var(--black); }
/* Only takes effect when .case__link is a direct grid child (the
   single-block .case__desc cases) — inside a .case__desc-col
   (the --split cases) it's just a normal block, unaffected. */
.case__desc > .case__link { grid-column: 2 / span 2; }

.case__desc {
  display: grid;
  grid-template-columns: 90px 1fr 1fr;
  align-items: start;
  gap: 16px;
  margin-bottom: 32px;
}
.case__desc p {
  grid-column: 2 / span 2;
  font-size: 12px;
  font-weight: 400;
  text-transform: uppercase;
  line-height: 1.6;
  margin: 0 0 14px;
  max-width: 640px;
}

/* Two distinct columns (role/contribution, then context/concept)
   instead of .case__desc's default single merged block. Column 1's
   width is solved algebraically (same approach as .about-bio-wrap in
   the About section) so column 2 lands exactly on the site's one
   canonical column-2 line — 45fr/55fr with a 96px gap, same ratio as
   .home-grid/.about-journey/.header-align, which is what visibly pins
   the RU/ENG toggle in the header. That canonical line sits at
   0.45*(100% - 96px) + 96px = 45% + 52.8px; subtract this row's own
   single 16px gap to get column 1's width (45% + 36.8px). Now that
   .case__lang no longer occupies the grid's first slot (see above),
   the role column starts where the RU/ENG toggle used to sit instead
   of being indented past it. */
.case__desc--split { grid-template-columns: calc(45% + 36.8px) minmax(0, 1fr); }
/* :nth-child(2)/(3), not (1)/(2) — .case__lang is still this grid's
   first DOM child even though display:none removes it from the
   rendered grid, so both .case__desc-col divs keep the same sibling
   index they'd have if it were visible. */
.case__desc--split .case__desc-col:nth-child(2) { grid-column: 1; }
.case__desc--split .case__desc-col:nth-child(3) { grid-column: 2; }
.case__desc--split p {
  font-size: 13px;
  font-weight: 400;
  text-transform: none;
  line-height: 1.4;
  margin: 0 0 12px;
  max-width: 480px;
}
.case__desc--split p:last-child { margin-bottom: 0; }

/* Desktop only: pulls the site-link out of the right column's own flow
   (where it used to sit above the paragraph, level with the role row
   instead of the title) and up onto the title/year row itself — left
   at 45% + 52.8px, the same canonical column-2 line .case__desc--split
   itself is built around (see that rule's own comment for the algebra),
   so it keeps the exact horizontal position it already had, just
   raised to a new top. Taking it out of flow here also does the other
   half of the job for free: the paragraph, now the right column's
   first child, lands on the role row's own line automatically, no
   separate rule needed for that. */
@media (min-width: 721px) {
  .case__desc--split .case__link {
    position: absolute;
    top: 32px;
    left: calc(45% + 52.8px);
    margin-bottom: 0;
  }
}

/* Right column of .case__desc--split as a collapsed-by-default
   accordion — same pattern as .exp-item__toggle/__panel in the About
   section's "Опыт работы", just sized for this row's 13px/1.4 body
   copy instead of that one's 15px. */
.case__role-toggle {
  display: flex;
  align-items: baseline;
  justify-content: flex-start;
  gap: 8px;
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
}
.case__role-title {
  font-size: 13px;
  font-weight: 400;
  line-height: 1.4;
}
.case__role-arrow {
  flex: none;
  width: 7px;
  height: 7px;
  margin-top: 4px;
  border-right: 1px solid var(--black);
  border-bottom: 1px solid var(--black);
  transform: rotate(-45deg);
  transition: transform 0.2s ease;
}
.case__role-toggle[aria-expanded="true"] .case__role-arrow { transform: rotate(45deg); }

/* max-height (JS-measured, see main.js), not the grid-template-rows
   0fr/1fr trick used elsewhere on the site (About's exp-item, Home's
   proj-block) — this panel's ancestor (.case__desc-col) switches to
   display:contents at mobile width for the role/link row layout, and
   that combination reportedly left some cases' text clipped or
   overlapping content below on mobile Safari (a transition on a grid
   track inside a flattened display:contents ancestor). max-height
   sidesteps it entirely by animating a plain block element instead. */
.case__role-panel {
  max-height: 0;
  overflow: hidden;
  transition: max-height 0.25s ease;
}
.case__role-panel-inner {
  min-height: 0;
  /* 13px, not About's own exp-item__panel-inner 8px — this column
     sits next to .case__desc-col's static paragraph (right column),
     whose own text starts 13px below the link/role row, so this
     lands the two paragraphs' first lines on the same row on desktop
     instead of the left one sitting ~5px higher. */
  padding-top: 13px;
}
.case__role-panel-inner p {
  font-size: 13px;
  font-weight: 400;
  line-height: 1.4;
  max-width: 480px;
  margin: 0;
}

/* Horizontal filmstrip — matches the source design: every case's
   material scrolls sideways at one fixed height, instead of wrapping
   into a grid. Add a new project's images in a row of <img> tags and
   it just works — no layout math needed. */
.case__gallery {
  display: flex;
  gap: 13px;
  overflow-x: auto;
  padding-bottom: 10px;
  scroll-snap-type: x proximity;
  /* Safari-specific: a flex container relying on its tallest item
     (the fixed-height img below) for its own height can, on reload,
     size itself off the image's un-clamped intrinsic height instead —
     reported on the Communications cases (single-image galleries) as
     the image blowing past its 300px cap. min-height:0 forces Safari
     to size the row from the flex item's used height, not its
     content's natural size. */
  min-height: 0;
  /* Images are loading="lazy" with only height fixed (width:auto), so
     each one's rendered width is unknown until it loads — as they load
     in one by one, the browser's default scroll-anchoring tries to
     "correct" the resulting horizontal layout shift by moving scrollLeft,
     landing the gallery scrolled to some mid/end position on load instead
     of showing image 1. Opting this container out of scroll anchoring
     stops that. */
  overflow-anchor: none;
}
.case__gallery img {
  height: 300px;
  width: auto;
  max-width: none;
  flex: 0 0 auto;
  object-fit: cover;
  scroll-snap-align: start;
  background: #f2f2f2;
  cursor: zoom-in;
}
.case__gallery::-webkit-scrollbar { height: 5px; }
.case__gallery::-webkit-scrollbar-track { background: transparent; }
.case__gallery::-webkit-scrollbar-thumb { background: var(--gray-line); border-radius: 3px; }

/* Case galleries wrapped like this (see projects.html) get the same
   always-visible "you can scroll this" affordance as About's carousel
   (.about-gallery-progress) — native scrollbars can auto-hide
   depending on OS settings, so a custom bar replaces it here instead
   of relying on it, plus a centered counter with prev/next arrows. */
.case__gallery-wrap .case__gallery {
  padding-bottom: 0;
  scrollbar-width: none;
}
.case__gallery-wrap .case__gallery::-webkit-scrollbar { display: none; }
.case__gallery-progress {
  height: 3px;
  margin-top: 8px;
  background: var(--gray-line);
  position: relative;
  overflow: hidden;
}
.case__gallery-progress__thumb {
  position: absolute;
  top: 0;
  left: 0;
  height: 100%;
  background: var(--gray-mid);
}
.case__gallery-counter {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  margin-top: 16px;
  font-size: 12px;
  font-weight: 400;
  letter-spacing: 0.04em;
  color: var(--gray-mid);
}
.case__empty {
  font-size: 12px;
  font-weight: 400;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--gray-mid);
  padding: 20px 0 0;
  margin-bottom: 32px;
}

/* Projects hero: same reference 32px above and below the lead statement
   on mobile, instead of the desktop-tuned --page-divider-gap and
   the empty .hero__left grid row (there for desktop column balance
   only) silently padding the gap out further. */
@media (max-width: 720px) {
  .page-hero--wide-gap { --page-divider-gap: 32px; }
  .page-hero--wide-gap .hero__grid { min-height: 0; }
  .page-hero--wide-gap .hero__left { display: none; }
  /* Matches the specificity of the shared -33px rule (.page-hero >
     .hero__grid .hero__bio--statement, 3 classes) so this override
     actually wins instead of silently losing the cascade. */
  .page-hero--wide-gap .hero__grid .hero__bio--statement { margin-top: 0; }
}

@media (max-width: 720px) {
  /* 720px, not the 760px this used to say — every other mobile/desktop
     split on the site (case__head, case__year--inline, etc.) uses 720;
     760 was a stray leftover that left a 721-760px gap where content
     was still getting the mobile-flattened layout despite being past
     the site's own desktop breakpoint. */
  /* gap:0 cancels .case__desc's own 16px row-gap (still set on the base
     rule) — without it, that stacks with the 32px margin-top below and
     the two columns end up 48px apart instead of the reference 32. */
  .case__desc { grid-template-columns: 1fr; gap: 0; }
  .case__desc p { grid-column: 1; }
  /* Role button and site link share one row here too, left/right, same
     as desktop's 3-column grid — each .case__desc-col becomes
     display:contents so its real children (role-row/role-panel,
     link/paragraph) become direct grid items of .case__desc--split
     instead of the two columns just stacking as separate blocks. The
     collapsible panel and the description paragraph still span the
     full width, each on its own row below. */
  .case__desc--split {
    display: grid;
    grid-template-columns: 1fr auto;
    align-items: baseline;
    column-gap: 16px;
    row-gap: 0;
  }
  .case__desc--split .case__desc-col { display: contents; }
  .case__desc--split .case__desc-col:nth-child(2) > .case__role-row {
    grid-column: 1;
    grid-row: 1;
  }
  .case__desc--split .case__desc-col:nth-child(3) > .case__link {
    grid-column: 2;
    grid-row: 1;
    justify-self: end;
    /* Handled by the paragraph's own margin-top below instead, now that
       the link and paragraph are no longer adjacent in the same column. */
    margin-bottom: 0;
  }
  .case__desc--split .case__desc-col:nth-child(2) > .case__role-panel {
    grid-column: 1 / -1;
    grid-row: 2;
  }
  /* 29px, not 32 — the role button's own bottom text-leading (~3px) and
     the link's own top text-leading (~3px) both sit inside this margin
     when going text-to-text, whereas the divider→button gap above only
     has ONE leading to absorb (a drawn line has none of its own). 29px
     of margin plus those two leadings lands the visually perceived gap
     at the same ~35px as above, instead of ~38px if left at 32.
     No grid-row here (unlike the row-1/row-2 items above) — a couple of
     cases (ATRY, Artisara) have TWO <p>s in this column, and hard-coding
     them both to the same row 3 stacked them on top of each other
     instead of one after another. Leaving row unset lets grid
     auto-placement give each its own row below .case__role-panel. */
  .case__desc--split .case__desc-col:nth-child(3) > p {
    grid-column: 1 / -1;
    margin-top: 0;
  }
  .case__desc--split .case__desc-col:nth-child(3) > p:first-of-type {
    margin-top: 29px;
  }
  .case__gallery img { height: 220px; }
  /* RU/ENG is an inert stub (no English copy exists) — not worth its own
     row once the description stacks to one column at this width. */
  .case__lang { display: none; }
}

/* ---------- Lightbox (click a gallery image to zoom) ---------- */

.lightbox {
  position: fixed;
  inset: 0;
  z-index: 1000;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 48px;
  background: rgba(10, 10, 10, 0.92);
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.3s ease;
  cursor: zoom-out;
}
.lightbox.is-open {
  opacity: 1;
  visibility: visible;
}
.lightbox img {
  max-width: 100%;
  max-height: 100%;
  width: auto;
  height: auto;
  object-fit: contain;
  transform: scale(0.92);
  transition: transform 0.3s ease;
  box-shadow: 0 30px 80px rgba(0, 0, 0, 0.5);
}
.lightbox.is-open img {
  transform: scale(1);
}

/* ---------- Footer ---------- */

.footer-divider {
  position: relative;
}
.footer-divider::after {
  content: "";
  position: absolute;
  left: 50%;
  right: 50%;
  bottom: 0;
  width: 100vw;
  margin-left: -50vw;
  border-bottom: 1px solid var(--black);
}

.site-footer {
  background: var(--white);
  color: var(--black);
}
.site-footer__inner {
  position: relative;
  padding-top: 32px;
  /* Same small gap as the header's own top padding, so the space below
     the last footer line matches the space above the header at the
     top of the page. */
  padding-bottom: 18px;
  display: grid;
  grid-template-columns: 45fr 55fr;
  grid-template-rows: auto 1fr;
  grid-template-areas:
    "portrait right"
    "credit   right";
  gap: 32px 96px;
}
.site-footer__portrait {
  grid-area: portrait;
  justify-self: start;
  width: clamp(133px, 16vw, 230px);
  aspect-ratio: 345 / 181;
}
.site-footer__portrait img { width: 100%; height: 100%; object-fit: contain; display: block; }
.site-footer__credit {
  grid-area: credit;
  align-self: end;
  display: flex;
  flex-direction: column;
  gap: 0;
  font-size: 13px;
  font-weight: 400;
  line-height: 1.2;
  color: var(--black);
}
.site-footer__right {
  grid-area: right;
  display: flex;
  flex-direction: column;
  gap: 32px;
}

/* Desktop only: place the "Let's Get Started" mark on the same upper
   edge as the right content column. The credit remains pinned to row 2;
   mobile keeps its own stacked layout below. */
@media (min-width: 721px) {
  .site-footer__portrait {
    grid-column: 1;
    grid-row: 1 / 3;
    align-self: start;
    justify-self: start;
    width: clamp(89px, 10.7vw, 153px);
  }
  /* The WebP itself has transparent pixels above the visible lettering
     (measured: ~9.4% of its own height) — offset the artwork, not the
     grid box, so its ink aligns with the top line of the right text
     column. A % offset (not a fixed px) keeps this correct as the
     portrait's own width/height clamp with viewport. */
  .site-footer__portrait img { transform: translateY(-9.4%); }
  .site-footer__credit { grid-column: 1; grid-row: 2; }
  .site-footer__right { grid-column: 2; grid-row: 1 / 3; }
}
.site-footer__nav,
.site-footer__social,
.site-footer__contact {
  display: flex;
  flex-direction: column;
  gap: 0;
  font-size: 13px;
  font-weight: 400;
  line-height: 1.2;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
.site-footer__nav a,
.site-footer__social a,
.site-footer__contact a {
  position: relative;
}
.site-footer__nav a::after,
.site-footer__social a::after,
.site-footer__contact a::after {
  content: "";
  position: absolute;
  top: -6px;
  bottom: -6px;
  left: -4px;
  right: -4px;
}
.site-footer__nav a { color: var(--black); }
.site-footer__nav a:hover { opacity: 0.6; }
.site-footer__social a,
.site-footer__social span {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  color: var(--black);
}
.site-footer__social a:hover { opacity: 0.6; }
.site-footer__social img { width: 10px; height: 10px; }
.site-footer__contact { text-transform: none; margin-top: auto; }
.site-footer__contact a { color: var(--black); }
.site-footer__contact a:hover { opacity: 0.6; }

@media (max-width: 720px) {
  .site-footer__inner {
    grid-template-columns: 1fr;
    grid-template-areas:
      "portrait"
      "right"
      "credit";
  }
  .site-footer__nav { display: none; }
}
@media (max-width: 640px) {
  .site-footer__inner { gap: 32px; }
}

/* Project titles, home cards and /projects cases alike: normal case at
   16px (the general .proj-block__cap / .case__title rules above are 13px
   uppercase), years at the same size so they share the title's line.
   Home cards also get a one-line grey subtitle saying what was done. */
.home-grid .proj-block__cap,
.case__title,
.case__year {
  font-size: 16px;
  text-transform: none;
  letter-spacing: 0;
}
.proj-block__sub {
  font-size: 13px;
  line-height: 1.4;
  color: var(--gray-mid);
  margin-top: 4px;
}
