/* ============================================================
   AR — Andrii Rymarchuk portfolio
   Design system: brand olive palette, bento grid, dual theme
   ============================================================ */

/* ---------- Fonts ----------
   TT Travels Next + Avanti are the display/accent faces from the
   previous site. They may not carry Cyrillic, so Inter is listed
   directly after them — the browser then falls back per-glyph and
   Ukrainian text stays readable in the same visual rhythm.        */

@font-face {
  font-family: 'TT Display';
  src: url('../fonts/tt-black-it.woff2') format('woff2');
  font-weight: 900; font-style: italic; font-display: swap;
}
@font-face {
  font-family: 'TT Display';
  src: url('../fonts/tt-xbold-it.woff2') format('woff2');
  font-weight: 800; font-style: italic; font-display: swap;
}
@font-face {
  font-family: 'TT Display';
  src: url('../fonts/tt-bold-it.woff2') format('woff2');
  font-weight: 700; font-style: italic; font-display: swap;
}
@font-face {
  font-family: 'TT Display';
  src: url('../fonts/tt-demibold-it.woff2') format('woff2');
  font-weight: 600; font-style: italic; font-display: swap;
}
@font-face {
  font-family: 'TT UI';
  src: url('../fonts/tt-bold.woff2') format('woff2');
  font-weight: 700; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'Avanti';
  src: url('../fonts/avanti-regular.woff2') format('woff2');
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'Avanti';
  src: url('../fonts/avanti-bold.woff2') format('woff2');
  font-weight: 700; font-style: normal; font-display: swap;
}

/* ---------- Tokens ---------- */

:root {
  --font-display: 'TT Display', 'Inter', system-ui, sans-serif;
  --font-ui: 'TT UI', 'Inter', system-ui, sans-serif;
  --font-hand: 'Avanti', 'Segoe Script', cursive;
  --font-body: 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;

  --r-card: 26px;
  --r-sm: 14px;
  --r-pill: 999px;

  --gap: 14px;
  --pad: clamp(18px, 2.4vw, 30px);
  --shell: 1240px;

  --ease: cubic-bezier(0.22, 1, 0.36, 1);
  --dur: 0.55s;

  --hdr-h: 68px;

  color-scheme: dark;
}

/* Every colour below traces back to one of the nine hex values from the
   brand palette. States that aren't in the palette directly (a lifted
   card, a soft divider) are mixed FROM those nine, never invented. */

:root,
:root[data-theme='dark'] {
  --bg: #1c1c16;
  --card: #1c1e19;
  --fg: #edefdd;
  --fg-dim: #a0a393;
  --accent: #c1c6b5;
  --accent-mute: #84877c;
  --line: #424438;

  --bg-deep: color-mix(in srgb, #1c1c16 88%, black 12%);
  --card-hi: color-mix(in srgb, #1c1e19 78%, #424438 22%);
  --line-soft: color-mix(in srgb, #424438 42%, #1c1e19 58%);

  --shadow: 0 18px 50px -22px rgba(0, 0, 0, 0.9);
  --img-veil: 0.86;
  color-scheme: dark;
}

:root[data-theme='light'] {
  --bg: #e5e8d8;
  --card: #edefdd;
  --fg: #1c1e19;
  --fg-dim: #686860;
  --accent: #424438;
  --accent-mute: #84877c;
  --line: #c1c6b5;

  --bg-deep: color-mix(in srgb, #e5e8d8 85%, black 15%);
  --card-hi: color-mix(in srgb, #edefdd 74%, #c1c6b5 26%);
  --line-soft: color-mix(in srgb, #c1c6b5 55%, #e5e8d8 45%);

  --shadow: 0 18px 46px -24px rgba(45, 48, 34, 0.4);
  --img-veil: 1;
  color-scheme: light;
}

@media (prefers-color-scheme: light) {
  :root:not([data-theme]) {
    --bg: #e5e8d8;
    --card: #edefdd;
    --fg: #1c1e19;
    --fg-dim: #686860;
    --accent: #424438;
    --accent-mute: #84877c;
    --line: #c1c6b5;

    --bg-deep: color-mix(in srgb, #e5e8d8 85%, black 15%);
    --card-hi: color-mix(in srgb, #edefdd 74%, #c1c6b5 26%);
    --line-soft: color-mix(in srgb, #c1c6b5 55%, #e5e8d8 45%);

    --shadow: 0 18px 46px -24px rgba(45, 48, 34, 0.4);
    --img-veil: 1;
    color-scheme: light;
  }
}

/* ---------- Base ---------- */

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

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

body {
  margin: 0;
  background: var(--bg);
  color: var(--fg);
  font-family: var(--font-body);
  font-size: 16px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  /* clip, not hidden: `hidden` would make <body> its own scroll container,
     which freezes window.scrollY and kills the sticky header + parallax. */
  overflow-x: clip;
  transition: background-color 0.4s var(--ease), color 0.4s var(--ease);
}

/* ---------- Ambient background ----------
   CSS/no-dependency stand-in for a requested WebGL gradient shader: soft
   drifting colour pools plus a faint grain, fixed behind every page.
   Built entirely from --card-hi / --accent-mute / --bg-deep — the same
   tokens the rest of the site already uses, each already computed
   per-theme by the :root[data-theme] blocks above — so this one rule
   reads correctly in dark and light with no separate light-mode branch,
   and --accent-mute is literally identical in both palettes, which is
   why it anchors the mix. A negative z-index on a positioned descendant
   paints after — i.e. on top of — its own ancestor's background-color in
   the stacking order, so this shows over body's solid var(--bg) fill
   while staying behind every real (z-index:auto or higher) element on
   the page. Fixed, not absolute: it should sit still like wallpaper, not
   travel with page content while scrolling. */
body::before {
  content: '';
  position: fixed;
  inset: -10%;
  z-index: -2;
  pointer-events: none;
  background:
    radial-gradient(40% 36% at 15% 20%, color-mix(in srgb, var(--card-hi) 75%, transparent), transparent 70%),
    radial-gradient(34% 30% at 85% 14%, color-mix(in srgb, var(--accent-mute) 34%, transparent), transparent 70%),
    radial-gradient(42% 38% at 82% 82%, color-mix(in srgb, var(--card-hi) 65%, transparent), transparent 70%),
    radial-gradient(36% 32% at 12% 85%, color-mix(in srgb, var(--bg-deep) 80%, transparent), transparent 70%);
  filter: blur(60px);
  opacity: 0.55;
  animation: bg-drift 46s var(--ease) infinite alternate;
  will-change: transform;
}
@keyframes bg-drift {
  0%   { transform: translate(0, 0) scale(1); }
  50%  { transform: translate(-2.5%, 2%) scale(1.05); }
  100% { transform: translate(2%, -2.5%) scale(1); }
}

/* Grain — inline feTurbulence, desaturated, tiled. A static raster the
   compositor repeats; no per-frame cost the way a canvas redraw would
   have, which is the whole point of doing this in CSS instead of the
   WebGL version. overlay blend reads it as texture, not visible speckling. */
body::after {
  content: '';
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  opacity: 0.05;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch' result='t'/%3E%3CfeColorMatrix in='t' type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 180px 180px;
}

@media (max-width: 700px) {
  /* Same layer, cheaper to composite: a 60px blur redrawn under animation
     is more GPU work than a phone's typically weaker GPU wants to spend
     on something purely decorative. */
  body::before { filter: blur(36px); opacity: 0.45; }
}

@media (prefers-reduced-motion: reduce) {
  body::before { animation: none; }
}

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

/* An author `display` beats the UA sheet's [hidden] rule, so anything given
   display:grid/flex stays on screen while "hidden". Make the attribute win. */
[hidden] { display: none !important; }
a { color: inherit; text-decoration: none; }
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }

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

.shell {
  width: min(100% - 2 * var(--pad), var(--shell));
  margin-inline: auto;
}

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

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

/* No bar spans the header anymore — .hdr is just a positioning wrapper,
   and the three groups inside it (logo, nav pills, tools) are each their
   own floating "island" with the page's own background showing through
   the gaps between them, not one continuous translucent strip. */
.hdr {
  position: sticky; top: 0; z-index: 60;
  height: var(--hdr-h);
  display: flex; align-items: center;
  pointer-events: none; /* islands re-enable it themselves */
}

.hdr__in {
  display: flex; align-items: center; gap: 14px;
  width: min(100% - 2 * var(--pad), var(--shell));
  margin-inline: auto;
}
.hdr__in > * { pointer-events: auto; }

.brand,
.nav,
.hdr__tools {
  border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--bg) 78%, transparent);
  backdrop-filter: blur(16px) saturate(1.3);
  -webkit-backdrop-filter: blur(16px) saturate(1.3);
  border: 1px solid var(--line-soft);
  box-shadow: 0 6px 20px -12px color-mix(in srgb, var(--bg-deep) 45%, transparent);
  transition: border-color 0.3s var(--ease), box-shadow 0.3s var(--ease);
}
/* A touch more definition once actually scrolled, so the islands still
   register as "now floating over content" the way the old full-width bar's
   border used to — just per-island instead of one edge-to-edge line.
   .nav is excluded below 1025px on purpose: it's a full-width dropdown
   sheet there (see the max-width: 1024px block), not an island, and its
   own border-bottom already means something different (the sheet's own
   edge) that this shouldn't reach in and retint. */
.hdr.is-stuck .brand,
.hdr.is-stuck .hdr__tools {
  border-color: color-mix(in srgb, var(--fg) 16%, var(--line-soft));
  box-shadow: 0 8px 24px -12px color-mix(in srgb, var(--bg-deep) 55%, transparent);
}
@media (min-width: 1025px) {
  .hdr.is-stuck .nav {
    border-color: color-mix(in srgb, var(--fg) 16%, var(--line-soft));
    box-shadow: 0 8px 24px -12px color-mix(in srgb, var(--bg-deep) 55%, transparent);
  }
}

.brand { display: flex; align-items: center; flex: 0 0 auto; padding: 7px 16px; }
.brand img { height: 30px; width: auto; }
.brand .logo-light { display: none; }
:root[data-theme='light'] .brand .logo-dark { display: none; }
:root[data-theme='light'] .brand .logo-light { display: block; }
@media (prefers-color-scheme: light) {
  :root:not([data-theme]) .brand .logo-dark { display: none; }
  :root:not([data-theme]) .brand .logo-light { display: block; }
}

.nav { display: flex; align-items: center; gap: 4px; margin-inline: auto; padding: 5px; position: relative; }
.nav a {
  position: relative; z-index: 1;
  font-family: var(--font-ui);
  font-size: 12px; letter-spacing: 0.08em;
  padding: 8px 14px; border-radius: var(--r-pill);
  color: var(--fg-dim);
  display: inline-flex; align-items: center; gap: 7px;
  transition: color 0.25s var(--ease);
  white-space: nowrap;
}
.nav__icon { width: 14px; height: 14px; flex: 0 0 auto; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }

/* "My products" is a different kind of destination from the portfolio pages
   beside it, so it sits apart: extra room plus a hairline divider. The line
   is absolutely placed in that gap, so it doesn't change the link's own box —
   nav.js sizes the sliding capsule from offsetLeft/offsetWidth. */
.nav a[href$="products.html"] { margin-left: 16px; }
.nav a[href$="products.html"]::before {
  content: ''; position: absolute; left: -10px; top: 24%; bottom: 24%;
  width: 1px; background: color-mix(in srgb, var(--fg) 30%, transparent);
}

/* Just the text goes dark on hover/active now — the capsule itself lives
   one level down, on .nav__indicator, so it can glide from one link to
   another instead of one popping out of existence while another pops in.
   .nav__lit, not [aria-current='page'] directly: the text needs to invert
   only on whichever link the capsule is actually sitting under right now,
   which is the hovered link as soon as one is, not the page link — nav.js
   moves this class together with the capsule. Tied to aria-current alone,
   the active page's link stayed light-colored text even after the capsule
   had already moved off it onto whatever was being hovered, which without
   a dark capsule underneath it any more read as the label just vanishing
   (light text on the same light bar). */
.nav a:hover,
.nav a.nav__lit { color: var(--bg); }

/* One shared capsule, positioned by nav.js (transform + width, in
   px measured off getBoundingClientRect — the only two properties that
   actually change between links) to sit under whichever link is hovered,
   falling back to the active page's link the instant the pointer leaves
   the bar. Same look the per-link pill had — vertical gradient, inset
   sheen top / shading bottom, an accent-tinted glow floating under it,
   a bright highlight blob across the top half via ::before — just one
   instance of it now, so moving between two links is the capsule
   sliding, not one instance dying and a different one being born in a
   different spot. Starts at width 0 so it's invisible until nav.js has
   measured a real target to sit under (no flash at the top-left corner
   on a slow layout pass); the width/transform transition covers the
   actual glide once it has somewhere to go. */
.nav__indicator {
  position: absolute; top: 5px; left: 0; bottom: 5px;
  width: 0;
  border-radius: var(--r-pill);
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--fg) 86%, white 14%) 0%,
    var(--fg) 48%,
    color-mix(in srgb, var(--fg) 84%, black 16%) 100%);
  box-shadow:
    inset 0 1px 0 color-mix(in srgb, var(--fg) 45%, white 55%),
    inset 0 -3px 6px color-mix(in srgb, var(--fg) 70%, black 30%),
    0 8px 18px -6px color-mix(in srgb, var(--accent) 55%, transparent),
    0 3px 8px -2px color-mix(in srgb, var(--bg-deep) 40%, transparent);
  transition: transform 0.4s var(--ease), width 0.4s var(--ease), opacity 0.2s var(--ease);
  pointer-events: none;
  z-index: 0;
}
.nav__indicator::before {
  content: '';
  position: absolute; inset: 2px 2px auto 2px;
  height: 48%;
  border-radius: inherit;
  background: linear-gradient(180deg, color-mix(in srgb, white 32%, transparent) 0%, transparent 100%);
}

.hdr__tools { display: flex; align-items: center; gap: 8px; flex: 0 0 auto; padding: 6px; }

.icon-btn {
  width: 36px; height: 36px; border-radius: 50%;
  display: grid; place-items: center;
  border: 1px solid var(--line);
  color: var(--fg-dim);
  transition: color 0.25s var(--ease), border-color 0.25s var(--ease),
              background-color 0.25s var(--ease), transform 0.25s var(--ease);
}
.icon-btn:hover { color: var(--fg); border-color: var(--accent-mute); background: var(--card); transform: translateY(-1px); }
.icon-btn svg { width: 17px; height: 17px; fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }

/* .icon-btn is `display:grid` with no template-columns, which puts every
   extra child on its own implicit ROW — flag stacked above label instead
   of beside it. Flex overrides that with an explicit left-to-right row. */
.lang-btn {
  display: inline-flex !important; flex-direction: row; align-items: center;
  width: auto; padding: 0 13px; gap: 7px;
  font-family: var(--font-ui); font-size: 11px; letter-spacing: 0.09em;
  /* .icon-btn's border-radius:50% turns a wide, fixed-height box into an
     ellipse/oval instead of a stadium — every other pill on the site
     (crumb, filter, btn-contact) uses --r-pill, so match that here too. */
  border-radius: var(--r-pill);
}
.lang-btn img { width: 17px; height: 12px; object-fit: cover; border-radius: 2px; }

/* The one button on the site that should be irresistible: solid fill,
   a permanent soft glow so it reads as "alive" even before the cursor
   arrives, and a light-sweep + lift on hover that makes it feel worth
   clicking rather than just another pill. */
a.btn-contact {
  position: relative;
  overflow: hidden;
  isolation: isolate;
  font-family: var(--font-ui); font-weight: 600; font-size: 12px; letter-spacing: 0.08em;
  padding: 9px 22px; border-radius: var(--r-pill);
  display: inline-flex; align-items: center; gap: 7px;
  /* Same glossy capsule gradient as the nav pills, so the whole header
     reads as one family of raised pill buttons rather than one glossy
     and one flat. */
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--fg) 86%, white 14%) 0%,
    var(--fg) 48%,
    color-mix(in srgb, var(--fg) 84%, black 16%) 100%);
  color: var(--bg);
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--fg) 40%, transparent),
              inset 0 1px 0 color-mix(in srgb, var(--fg) 45%, white 55%),
              inset 0 -3px 6px color-mix(in srgb, var(--fg) 70%, black 30%),
              0 6px 20px -6px color-mix(in srgb, var(--accent) 65%, transparent);
  transition: transform 0.35s var(--ease), box-shadow 0.35s var(--ease);
  white-space: nowrap;
  animation: contact-glow 3.2s ease-in-out infinite;
}
/* Top-half highlight blob — overflow:hidden on the button clips it to the
   pill, so no inset/border-radius juggling needed here. Sits behind the
   text either way (z-index -1, same layer as the sweep below). */
a.btn-contact::after {
  content: '';
  position: absolute; inset: 0 0 50% 0;
  z-index: -1;
  background: linear-gradient(180deg, color-mix(in srgb, white 32%, transparent) 0%, transparent 100%);
  pointer-events: none;
}
/* Diagonal light sweep, parked off-frame until hover. */
a.btn-contact::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  background: linear-gradient(115deg,
    transparent 20%,
    color-mix(in srgb, var(--bg) 55%, transparent) 50%,
    transparent 80%);
  transform: translateX(-130%);
  transition: transform 0.65s var(--ease);
}
a.btn-contact:hover {
  transform: translateY(-2px) scale(1.035);
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--fg) 55%, transparent),
              inset 0 1px 0 color-mix(in srgb, var(--fg) 45%, white 55%),
              inset 0 -3px 6px color-mix(in srgb, var(--fg) 70%, black 30%),
              0 10px 28px -8px color-mix(in srgb, var(--accent) 85%, transparent);
  animation-play-state: paused;
}
a.btn-contact:hover::before { transform: translateX(130%); }
a.btn-contact:active { transform: translateY(0) scale(0.98); }

@keyframes contact-glow {
  0%, 100% {
    box-shadow: 0 0 0 1px color-mix(in srgb, var(--fg) 40%, transparent),
                inset 0 1px 0 color-mix(in srgb, var(--fg) 45%, white 55%),
                inset 0 -3px 6px color-mix(in srgb, var(--fg) 70%, black 30%),
                0 6px 20px -6px color-mix(in srgb, var(--accent) 55%, transparent);
  }
  50% {
    box-shadow: 0 0 0 1px color-mix(in srgb, var(--fg) 40%, transparent),
                inset 0 1px 0 color-mix(in srgb, var(--fg) 45%, white 55%),
                inset 0 -3px 6px color-mix(in srgb, var(--fg) 70%, black 30%),
                0 8px 26px -6px color-mix(in srgb, var(--accent) 90%, transparent);
  }
}

@media (prefers-reduced-motion: reduce) {
  a.btn-contact { animation: none; }
  a.btn-contact::before { display: none; }
}

.nav-toggle { display: none; }

/* Two contact buttons live in the markup: the header one is for desktop,
   the in-nav one only appears inside the mobile drawer. */
.nav a.btn-contact { display: none; }

/* Icon pairs swap on state instead of being rebuilt in JS. */
.icon-btn .i-moon,
.icon-btn[data-state='light'] .i-sun { display: none; }
.icon-btn[data-state='light'] .i-moon { display: block; }

.nav-toggle .i-close { display: none; }
.nav-toggle[aria-expanded='true'] .i-menu { display: none; }
.nav-toggle[aria-expanded='true'] .i-close { display: block; }

/* Tablet gets the same collapsed hamburger nav as phone — inline nav items
   plus EN/theme/contact were crowding awkwardly on iPad-width viewports. */
@media (max-width: 1024px) {
  .nav {
    position: fixed; inset: var(--hdr-h) 0 auto 0;
    flex-direction: column; align-items: stretch; gap: 2px;
    background: var(--bg); border-bottom: 1px solid var(--line-soft);
    padding: 14px var(--pad) 22px;
    margin: 0;
    /* The floating-island treatment above (rounded, backdrop-blurred,
       shadowed) is a desktop-header thing — reset it all here, or a
       rounded, floating card is what tries to become a full-width
       dropdown sheet. */
    border-radius: 0; border-inline: 0; border-top: 0;
    backdrop-filter: none; -webkit-backdrop-filter: none;
    box-shadow: none;
    transform: translateY(-12px); opacity: 0; pointer-events: none;
    transition: transform 0.3s var(--ease), opacity 0.3s var(--ease);
  }
  .nav.is-open { transform: none; opacity: 1; pointer-events: auto; }
  .nav a { font-size: 15px; padding: 13px 16px; }
  /* The inverted text of .nav__lit only works with the capsule sitting
     under it, and the capsule is hidden at this width — nav.js still marks
     the current page's link, so without this the drawer rendered that one
     item as dark text on the drawer's own dark background. */
  .nav a.nav__lit { color: var(--fg); }
  .nav a[href$="products.html"] { margin-left: 0; margin-top: 12px; }
  .nav a[href$="products.html"]::before {
    left: 16px; right: 16px; top: -7px; bottom: auto; width: auto; height: 1px;
  }
  .nav-toggle { display: grid; }
  .hdr__tools a.btn-contact.hide-sm { display: none; }
  /* flex, not block: block dropped the icon/label gap the pill sets up, so
     the arrow ran straight into the first letter. */
  .nav a.btn-contact {
    display: flex; justify-content: center; gap: 7px;
    text-align: center; margin-top: 10px;
  }
}

/* ---------- Bento grid ---------- */

.bento {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  gap: var(--gap);
  padding-block: clamp(18px, 3vw, 34px) clamp(40px, 6vw, 76px);
}

.card {
  position: relative;
  background: var(--card);
  border: 1px solid var(--line-soft);
  border-radius: var(--r-card);
  padding: var(--pad);
  overflow: hidden;
  isolation: isolate;
  transition: transform 0.45s var(--ease), border-color 0.45s var(--ease),
              background-color 0.45s var(--ease);
}
a.card:hover, .card.is-hoverable:hover {
  transform: translateY(-4px);
  border-color: var(--accent-mute);
  background: var(--card-hi);
}

.card__kicker {
  font-family: var(--font-ui);
  font-size: 10px; letter-spacing: 0.16em;
  color: var(--fg-dim); text-transform: uppercase;
  margin: 0 0 6px;
}
.card__title {
  font-family: var(--font-display);
  font-weight: 700; font-style: italic;
  /* Ukrainian labels run long; never break a word, shrink instead. */
  font-size: clamp(16px, 1.6vw, 21px);
  letter-spacing: -0.01em;
  margin: 0;
  overflow-wrap: normal; hyphens: manual;
}
.c-video .card__title,
.c-design .card__title,
.c-profile .card__title { font-size: clamp(15px, 1.4vw, 19px); }

/* circular arrow affordance, bottom-right of a card */
.card__go {
  position: absolute; right: var(--pad); bottom: var(--pad);
  width: 38px; height: 38px; border-radius: 50%;
  display: grid; place-items: center;
  border: 1px solid var(--line);
  color: var(--fg-dim);
  transition: transform 0.4s var(--ease), color 0.3s var(--ease),
              border-color 0.3s var(--ease), background-color 0.3s var(--ease);
  z-index: 3;
}
.card__go svg { width: 15px; height: 15px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
a.card:hover .card__go, .card.is-hoverable:hover .card__go {
  color: var(--bg); background: var(--fg); border-color: var(--fg);
  transform: rotate(-45deg);
}

.card__foot {
  position: relative; z-index: 2;
  margin-top: auto;
  padding-right: 54px; /* clears the circular arrow */
  min-width: 0;
}

/* ---------- Home cards ---------- */

/* Rows resolve to 5+4+3 / (hero)+3+4 / 3+4+5 — always twelve columns. */
/* Explicit placement, because .hero-portrait shares this exact cell —
   with auto-placement the portrait claimed it and shoved the card aside. */
.c-hero    { grid-column: 1 / span 6; grid-row: 1 / span 2; min-height: 420px; }
.c-about   { grid-column: span 3; display: flex; flex-direction: column; min-height: 210px; }
.c-video   { grid-column: span 3; display: flex; flex-direction: column; min-height: 210px; }
.c-design  { grid-column: span 3; display: flex; flex-direction: column; min-height: 210px; }
.c-offer   { grid-column: span 3; display: flex; flex-direction: column; min-height: 210px; }
.c-profile { grid-column: span 3; display: flex; flex-direction: column; min-height: 200px; }
.c-stats   { grid-column: span 5; padding: calc(var(--pad) * 0.5); }
.c-cta     { grid-column: span 4; display: flex; flex-direction: column; justify-content: flex-end; min-height: 200px; }

/* Full-width marquee band sitting above the grid. */
.ticker-strip {
  padding: 0; display: flex; align-items: center;
  height: 58px; margin-bottom: var(--gap);
}

/* Tablet/phone drop the 12-column track set entirely rather than keeping it
   and spanning across. Twelve `1fr` tracks at phone width are ~28px each, and
   `1fr` means `minmax(auto, 1fr)` — any card whose min-content is wider than
   its share blows the tracks out to ragged, unequal widths, which is what
   made the cards land two-up at mismatched sizes. Two real columns on tablet,
   one on phone, both with `minmax(0, 1fr)` so a track may shrink below its
   content instead of forcing the grid wider. */
@media (max-width: 1024px) {
  .bento { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .bento > .card { grid-column: auto / span 1; grid-row: auto; }
  /* `.bento >` prefix on purpose: `.bento > .card` above is two classes and
     would otherwise outrank a bare `.c-hero`, resetting grid-row to auto and
     dropping the portrait out of the card's cell into a row of its own. */
  .bento > .c-hero { grid-column: 1 / -1; grid-row: 1; }
  /* Stats stays half-width so it pairs with .c-profile — made full-width it
     pushed profile into a row on its own with dead space beside it. */
  .bento > .c-cta { grid-column: 1 / -1; }

  /* Video editing leads graphic design here. The source order puts design
     first because of where the two sit in the desktop grid, which stops
     mattering once the cards stack — and video is the main offering, so it
     should be the first thing reached after scrolling. Every auto-placed
     card needs an explicit order, otherwise the untouched ones (order 0)
     would all jump ahead of the two being reordered. */
  .bento > .c-about   { order: 1; }
  .bento > .c-video   { order: 2; }
  .bento > .c-design  { order: 3; }
  .bento > .c-offer   { order: 4; }
  .bento > .c-profile { order: 5; }
  .bento > .c-stats   { order: 6; }
  .bento > .c-cta     { order: 7; }
}
@media (max-width: 700px) {
  .bento { grid-template-columns: minmax(0, 1fr); }
  .bento > .card { grid-column: 1 / -1; }
  .bento > .c-hero { grid-row: 1; }
}

/* ---------- Hero card ----------
   The portrait is a transparent cut-out, not a framed photo — it sits flush
   in the card's own padding corner with no background of its own, and dips
   past the padding line so it reads as stepping out of the cell rather than
   being boxed inside it. */
.c-hero {
  display: flex; flex-direction: column;
  position: relative;
}

.hero__meta { min-width: 0; }
.hero__role {
  font-family: var(--font-ui); font-size: 10px; letter-spacing: 0.18em;
  color: var(--fg-dim); margin: 0 0 8px;
}
.hero__name {
  font-family: var(--font-display); font-weight: 900; font-style: italic;
  font-size: clamp(28px, 3.2vw, 44px); line-height: 1.02;
  letter-spacing: -0.025em; margin: 0 0 10px;
  overflow-wrap: normal; hyphens: manual;
}
.hero__tagline {
  font-family: var(--font-ui); font-size: 11px; letter-spacing: 0.1em;
  color: var(--fg-dim); margin: 0; text-transform: uppercase;
}
.hero__quote {
  font-family: var(--font-hand);
  font-size: clamp(17px, 1.8vw, 22px); line-height: 1.45;
  color: var(--fg); margin: 22px 0 0;
  max-width: 40ch; position: relative; z-index: 2;
}

/* ticker */
.ticker { overflow: hidden; width: 100%; mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent); }
.ticker__track { display: flex; width: max-content; will-change: transform; }
.ticker__track span {
  font-family: var(--font-ui); font-size: 12px; letter-spacing: 0.2em;
  color: var(--fg-dim); padding-right: 28px; white-space: nowrap;
}

/* media preview inside cards — kept clearly visible, not a faint hint. */
.card__media {
  position: absolute; inset: 0; z-index: 0;
  opacity: 0.72; filter: saturate(0.85);
  transition: opacity 0.5s var(--ease), transform 0.9s var(--ease);
}
.card__media img, .card__media video { width: 100%; height: 100%; object-fit: cover; }
a.card:hover .card__media { opacity: 0.85; transform: scale(1.04); }
/* The design teaser carries readable client copy — keep it a touch calmer. */
.c-design .card__media { opacity: 0.55; }
a.c-design:hover .card__media { opacity: 0.68; }
/* Scrim keeps only the kicker/title corner legible; the rest of the
   artwork stays clear rather than washed out under a heavy veil. */
.card__media::after {
  content: ''; position: absolute; inset: 0;
  background: linear-gradient(180deg,
    transparent 0%,
    transparent 40%,
    color-mix(in srgb, var(--card) 55%, transparent) 74%,
    color-mix(in srgb, var(--card) 92%, transparent) 100%);
}

/* services — each icon sits right beside its own label, not in a
   separate row disconnected from the text it belongs to. */
.offer__icon {
  width: 30px; height: 30px; border-radius: 9px; flex: 0 0 auto;
  display: grid; place-items: center;
  border: 1px solid var(--line-soft); color: var(--fg-dim);
  transition: color 0.3s var(--ease), border-color 0.3s var(--ease), transform 0.3s var(--ease);
}
.card:hover .offer__icon { color: var(--fg); border-color: var(--accent-mute); }
.offer__icon svg { width: 15px; height: 15px; fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
.offer__list { list-style: none; margin: 0 0 22px; padding: 0; columns: 2; column-gap: 26px; }
.offer__list li {
  display: flex; align-items: center; gap: 10px;
  font-size: 13.5px; color: var(--fg-dim); padding: 5px 0; break-inside: avoid;
}
@media (max-width: 620px) { .offer__list { columns: 1; } }

/* profiles */
.profile__row { display: flex; gap: 10px; margin-bottom: auto; }
.profile__dot {
  width: 54px; height: 54px; border-radius: 50%;
  display: grid; place-items: center;
  background: var(--card-hi); border: 1px solid var(--line-soft);
  color: var(--fg-dim);
  transition: transform 0.4s var(--ease), color 0.3s var(--ease);
}
.card:hover .profile__dot { color: var(--fg); }
.card:hover .profile__dot:first-child { transform: translateX(-3px) rotate(-6deg); }
.card:hover .profile__dot:last-child { transform: translateX(3px) rotate(6deg); }
.profile__dot svg {
  width: 22px; height: 22px;
  fill: none; stroke: currentColor; stroke-width: 1.7;
  stroke-linecap: round; stroke-linejoin: round;
}

/* stats */
.stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; height: 100%; }
.stat {
  background: color-mix(in srgb, var(--card-hi) 60%, transparent);
  border: 1px solid var(--line-soft);
  border-radius: 18px;
  padding: clamp(16px, 2.2vw, 26px) 10px;
  text-align: center;
  display: flex; flex-direction: column; justify-content: center; gap: 8px;
  min-width: 0; /* four-digit years otherwise force the grid to overflow */
}
.stat__num {
  font-family: var(--font-display); font-weight: 900; font-style: italic;
  font-size: clamp(24px, 2.8vw, 38px); line-height: 1; letter-spacing: -0.03em;
}
.stat__lbl { font-family: var(--font-ui); font-size: 9.5px; letter-spacing: 0.14em; color: var(--fg-dim); line-height: 1.5; }

/* cta */
.cta__title {
  font-family: var(--font-display); font-weight: 900; font-style: italic;
  /* "Працюймо" is a single long word — size so it fits the card unbroken. */
  font-size: clamp(28px, 3.2vw, 46px); line-height: 1.02;
  letter-spacing: -0.03em; margin: 0; padding-right: 58px;
  overflow-wrap: normal; hyphens: manual;
}
.cta__title .hand {
  font-family: var(--font-hand); font-style: normal; font-weight: 700;
  color: var(--accent); letter-spacing: 0;
}
/* Top-right, clear of the title text (which is left-aligned and can run
   two lines tall) — it used to sit top-left and overlap the first line. */
.cta__spark {
  position: absolute; right: var(--pad); top: var(--pad);
  width: 30px; height: 30px; color: var(--accent-mute); opacity: 0.8;
}
.cta__spark svg { width: 100%; height: 100%; fill: currentColor; }

/* ---------- Page heading ---------- */

.page-head {
  padding-block: clamp(26px, 5vw, 60px) clamp(18px, 3vw, 34px);
  text-align: center;
}
.page-head__title {
  font-family: var(--font-display); font-weight: 900; font-style: italic;
  font-size: clamp(38px, 7.5vw, 92px); line-height: 0.86;
  letter-spacing: -0.04em; margin: 0;
  text-transform: lowercase;
}
.page-head__title .outline {
  /* Was rendered in a separate outline typeface (transparent fill +
     stroke) — now just inherits .page-head__title's own solid weight,
     same as the word above it. display:block is the only thing this
     still needs to do: force the second word onto its own line. */
  display: block;
}
/* .page-head__title's line-height (0.86, tight on purpose for the big
   italic display type) is shorter than the font's actual descender —
   fine normally, but the word-cascade intro wraps each word in a
   .tw-mask box with overflow:hidden for the reveal sweep, and that box
   inherits the same tight line-height, clipping descenders like the "g"
   in "editing". Give just the mask box (not the heading's own line
   spacing) enough room to clear them. */
.page-head__title .tw-mask { line-height: 1.25; }
.page-head__sub {
  font-family: var(--font-hand);
  font-size: clamp(16px, 2vw, 22px); color: var(--fg-dim);
  margin: 16px 0 0;
}

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

/* Animated hatch-effect photo behind the whole page — see
   assets/js/ascii-bg.js. Fixed so it doesn't scroll away on a short page,
   and dimmed + scrimmed in CSS (on top of the effect's own vignette)
   rather than made subtle purely in the canvas, so the heading and cards
   never fight it for legibility no matter how the photo's own contrast
   happens to fall. The .glass cards' backdrop-filter sits above this and
   frosts it further, same as it does the page behind any other card. */
.contact-bg {
  position: fixed; inset: 0;
  z-index: -1; /* a positioned z-index:auto box still paints AFTER static
                  in-flow content per the stacking spec, DOM order or not —
                  negative is what actually puts this behind the page. */
  overflow: hidden;
  pointer-events: none;
}
/* Starts fully transparent and fades up to its resting 0.55 once the
   photo has actually loaded and the first frame is drawn (ascii-bg.js
   adds .is-in then) — a plain page-open reveal, not scroll-triggered, so
   it doesn't use the site's [data-reveal] IntersectionObserver machinery. */
.contact-bg canvas {
  display: block; width: 100%; height: 100%;
  opacity: 0;
  transition: opacity 3s var(--ease);
}
.contact-bg canvas.is-in { opacity: 0.55; }
@media (prefers-reduced-motion: reduce) {
  .contact-bg canvas { transition: none; }
}
.contact-bg::after {
  content: '';
  position: absolute; inset: 0;
  background:
    radial-gradient(ellipse 70% 60% at 50% 30%, transparent 0%, color-mix(in srgb, var(--bg) 55%, transparent) 60%, var(--bg) 100%),
    linear-gradient(180deg, color-mix(in srgb, var(--bg) 35%, transparent) 0%, transparent 20%, transparent 75%, var(--bg) 100%);
}

.contact { position: relative; z-index: 1; padding-block: clamp(6px, 2vw, 18px) clamp(10px, 3vw, 26px); }
/* One column rather than a grid: the email address is long, and side by
   side at this width it had nowhere to go but an ugly mid-word break. */
.contact__list {
  list-style: none; margin: 0 auto; padding: 0;
  max-width: 600px;
  display: grid; gap: 14px;
}
/* The rise lives on this plain wrapper, not the .glass card itself —
   .glass[data-reveal] is pinned to opacity-only on purpose (moving a
   backdrop-filter is costly), and with just two cards here the wrapper
   carrying it along is cheap enough to get the lift back. */
.contact-card {
  display: flex; align-items: center; gap: 16px;
  padding: 16px 18px;
  border: 1px solid var(--line);
  border-radius: var(--r-card);
  color: var(--fg);
  transition: transform 0.35s var(--ease), box-shadow 0.35s var(--ease);
}
/* .glass paints its rim and cursor glow on absolutely-positioned pseudo
   layers; unpositioned children would sit underneath them. */
.contact-card > * { position: relative; }
.contact-card:hover {
  transform: translateY(-4px);
  box-shadow: 0 18px 36px -18px color-mix(in srgb, var(--bg-deep) 70%, transparent);
}
/* Same glossy capsule as the contact button and the nav pill, so the
   icons read as part of the site rather than third-party brand marks. */
.contact-card__icon {
  flex: 0 0 auto;
  width: 52px; height: 52px; border-radius: 50%;
  display: grid; place-items: center;
  color: var(--bg);
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--fg) 86%, white 14%) 0%,
    var(--fg) 48%,
    color-mix(in srgb, var(--fg) 84%, black 16%) 100%);
  box-shadow:
    inset 0 1px 0 color-mix(in srgb, var(--fg) 45%, white 55%),
    inset 0 -3px 6px color-mix(in srgb, var(--fg) 70%, black 30%),
    0 6px 16px -6px color-mix(in srgb, var(--accent) 60%, transparent);
  transition: transform 0.4s var(--ease);
}
.contact-card:hover .contact-card__icon { transform: rotate(-6deg) scale(1.05); }
.contact-card__icon svg {
  width: 24px; height: 24px;
  fill: none; stroke: currentColor; stroke-width: 1.7;
  stroke-linecap: round; stroke-linejoin: round;
}
.contact-card__text { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 4px; }
.contact-card__label {
  font-family: var(--font-ui);
  font-size: 10px; letter-spacing: 0.16em;
  color: var(--fg-dim); text-transform: uppercase;
}
.contact-card__value {
  font-family: var(--font-display); font-weight: 700; font-style: italic;
  font-size: clamp(15px, 1.6vw, 19px); letter-spacing: -0.01em;
  overflow-wrap: anywhere;
}
.contact-card__go {
  flex: 0 0 auto;
  width: 38px; height: 38px; border-radius: 50%;
  display: grid; place-items: center;
  border: 1px solid var(--line);
  color: var(--fg-dim);
  transition: transform 0.4s var(--ease), color 0.3s var(--ease),
              border-color 0.3s var(--ease), background-color 0.3s var(--ease);
}
.contact-card__go svg { width: 15px; height: 15px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
.contact-card:hover .contact-card__go {
  color: var(--bg); background: var(--fg); border-color: var(--fg);
  transform: translateX(3px);
}
@media (max-width: 480px) {
  .contact-card { gap: 12px; padding: 14px; }
  .contact-card__icon { width: 44px; height: 44px; }
  .contact-card__icon svg { width: 21px; height: 21px; }
  .contact-card__go { display: none; }
}

/* breadcrumb / quick-nav row — only the Back pill carries an arrow, so the
   row doesn't read as three identical, confusing directional cues. */
.crumbs { display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; padding-block: 22px 22px; }
/* Used to be a bare outline in --fg-dim — against this site's own
   near-identical --bg/--card tones that read as almost no button at all
   until you happened to hover it. A real background fill + shadow now
   gives it presence at rest, not just on hover. */
.crumb {
  font-family: var(--font-ui); font-size: 11px; letter-spacing: 0.1em;
  padding: 9px 17px; border-radius: var(--r-pill);
  border: 1px solid var(--line); color: var(--fg);
  background: color-mix(in srgb, var(--fg) 7%, var(--card));
  box-shadow: 0 1px 3px color-mix(in srgb, var(--bg-deep) 20%, transparent);
  display: inline-flex; align-items: center; gap: 7px;
  transition: color 0.25s var(--ease), border-color 0.25s var(--ease),
              background-color 0.25s var(--ease), transform 0.25s var(--ease);
}
/* Same fg/bg inversion as the nav chips — a crumb that's actually being
   hovered should look pressable, not just faintly tinted. */
.crumb:hover { color: var(--bg); border-color: var(--fg); background: var(--fg); transform: translateY(-1px); }
.crumb svg { width: 13px; height: 13px; fill: none; stroke: currentColor; stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round; }
.crumb--back { border-style: dashed; }

/* ---------- Category grid (video hub) ---------- */

.cat-grid {
  display: grid; gap: var(--gap);
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  padding-bottom: clamp(30px, 5vw, 60px);
}
.cat {
  position: relative; display: flex; flex-direction: column; justify-content: flex-end;
  aspect-ratio: 16 / 10; padding: var(--pad);
  border-radius: var(--r-card); overflow: hidden;
  border: 1px solid var(--line-soft); background: var(--card);
  transition: transform 0.45s var(--ease), border-color 0.45s var(--ease);
}
.cat:hover { transform: translateY(-5px); border-color: var(--accent-mute); }
.cat video, .cat img.cat__poster {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; z-index: 0;
  transition: transform 0.9s var(--ease), opacity 0.5s var(--ease);
  opacity: var(--img-veil);
}
.cat:hover video { transform: scale(1.05); }
.cat::after {
  content: ''; position: absolute; inset: 0; z-index: 1;
  background: linear-gradient(180deg, rgba(10,12,8,0.05) 0%, rgba(10,12,8,0.55) 55%, rgba(10,12,8,0.9) 100%);
}
.cat__body { position: relative; z-index: 2; color: #edefdd; }
.cat__name {
  font-family: var(--font-display); font-weight: 800; font-style: italic;
  font-size: clamp(17px, 2vw, 23px); line-height: 1.1; margin: 0 0 4px;
  letter-spacing: -0.01em;
}
.cat__watch {
  font-family: var(--font-ui); font-size: 10px; letter-spacing: 0.16em;
  color: rgba(237,239,221,0.72); display: inline-flex; align-items: center; gap: 6px;
}
.cat__watch svg { width: 12px; height: 12px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.cat__count {
  position: absolute; top: var(--pad); right: var(--pad); z-index: 2;
  font-family: var(--font-ui); font-size: 10px; letter-spacing: 0.1em;
  padding: 5px 11px; border-radius: var(--r-pill);
  background: rgba(20,22,16,0.55); color: #edefdd;
  backdrop-filter: blur(8px);
}

/* locked variant — full-bleed banner, capped so it stays a band */
.cat--locked { aspect-ratio: 3 / 1; max-height: 300px; margin-top: var(--gap); }
.cat--locked .cat__name { font-size: clamp(20px, 2.6vw, 30px); }
.locked-note {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  font-size: 13px; color: rgba(237,239,221,0.78); margin: 0 0 10px;
  max-width: 52ch;
}
.locked-note svg { width: 15px; height: 15px; flex: 0 0 auto; fill: none; stroke: currentColor; stroke-width: 1.8; }

/* ---------- Video grid ---------- */

.vid-grid {
  display: grid; gap: var(--gap);
  /* Big by default — this is a video editor's reel, not a thumbnail wall. */
  grid-template-columns: repeat(auto-fill, minmax(560px, 1fr));
  padding-bottom: clamp(30px, 5vw, 60px);
}
.vid-grid.is-vertical { grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); }

.vid {
  position: relative;
  border-radius: var(--r-card); overflow: hidden;
  border: 1px solid var(--line-soft); background: var(--card);
  transition: transform 0.4s var(--ease), border-color 0.4s var(--ease);
}
.vid:hover { transform: translateY(-4px); border-color: var(--accent-mute); }

.vid__frame {
  position: relative; width: 100%; aspect-ratio: 16 / 9;
  background: var(--bg-deep); cursor: pointer; display: block; border: 0; padding: 0;
  overflow: hidden;
}
.is-vertical .vid__frame { aspect-ratio: 9 / 16; }
.vid__frame img {
  width: 100%; height: 100%; object-fit: cover;
  transition: transform 0.7s var(--ease), opacity 0.4s var(--ease);
  opacity: 0.92;
}
.vid__frame:hover img { transform: scale(1.05); opacity: 1; }
.vid__frame iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
/* Silent hover/touch preview — same persistent-wrapper trick as
   .deck__preview: YT.Player replaces whatever node it mounts into, so the
   pointer-events:none has to live on a box around that node, not on the
   node itself, for clicks to keep falling through to the button beneath. */
.vid__preview { position: absolute; inset: 0; pointer-events: none; }
/* Poster and play badge only step aside once the player actually reports
   PLAYING (see is-previewing in render.js) — a clip with embedding
   disabled must never leave an empty black box behind. Same specificity
   as .vid__frame:hover img (0,2,1): this rule must stay below it in the
   file so a hovered *and* previewing card still fades correctly. */
.vid__frame.is-previewing img { opacity: 0; }
.vid__frame.is-previewing .vid__play { opacity: 0; }
.vid__play {
  position: absolute; inset: 0; display: grid; place-items: center;
  transition: opacity 0.3s var(--ease);
}
.vid__play span {
  width: 56px; height: 56px; border-radius: 50%;
  display: grid; place-items: center;
  background: rgba(20,22,16,0.55); backdrop-filter: blur(10px);
  border: 1px solid rgba(237,239,221,0.28); color: #edefdd;
  transition: transform 0.35s var(--ease), background-color 0.3s var(--ease);
}
.vid__frame:hover .vid__play span { transform: scale(1.08); background: rgba(20,22,16,0.75); }
.vid__play svg { width: 18px; height: 18px; fill: currentColor; margin-left: 3px; }
.vid__cap {
  font-family: var(--font-ui); font-size: 10.5px; letter-spacing: 0.09em;
  color: var(--fg-dim); padding: 13px var(--pad) 15px; line-height: 1.5;
}

/* ---------- Design gallery ---------- */

.filters { display: flex; flex-wrap: wrap; gap: 8px; padding-bottom: 22px; }
.filter {
  font-family: var(--font-ui); font-size: 11px; letter-spacing: 0.1em;
  padding: 10px 18px; border-radius: var(--r-pill);
  border: 1px solid var(--line); color: var(--fg-dim);
  transition: all 0.28s var(--ease);
}
.filter:hover { color: var(--fg); border-color: var(--accent-mute); }
.filter[aria-pressed='true'] { background: var(--fg); color: var(--bg); border-color: var(--fg); }

.work-grid {
  display: grid; gap: var(--gap);
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  padding-bottom: clamp(30px, 5vw, 60px);
}
/* .work is itself the clickable trigger (a real <button>, for the click
   delegation + keyboard semantics that come free with that), and a button
   can't legally contain another button — this wrapper is what makes the
   share button possible as a sibling overlay instead. */
.work-item { position: relative; border-radius: var(--r-card); }
.work {
  position: relative; display: block;
  border-radius: var(--r-card); overflow: hidden;
  border: 1px solid var(--line-soft); background: var(--card);
  cursor: zoom-in;
  transition: transform 0.4s var(--ease), border-color 0.4s var(--ease), opacity 0.35s var(--ease);
}
.work:hover { transform: translateY(-4px); border-color: var(--accent-mute); }
.work img { width: 100%; aspect-ratio: 16 / 9; object-fit: cover; transition: transform 0.8s var(--ease); }
.work:hover img { transform: scale(1.05); }
.work__cap {
  font-family: var(--font-ui); font-size: 10px; letter-spacing: 0.1em;
  color: var(--fg-dim); padding: 13px var(--pad) 15px; line-height: 1.5;
}
.work.is-hidden { display: none; }
/* Marks a project that has more than one banner variant behind it. */
.work__count {
  position: absolute; top: 12px; right: 12px; z-index: 2;
  display: inline-flex; align-items: center; gap: 5px;
  font-family: var(--font-ui); font-size: 10px; letter-spacing: 0.08em;
  padding: 5px 10px; border-radius: var(--r-pill);
  background: rgba(20,22,16,0.6); color: #edefdd; backdrop-filter: blur(8px);
}
.work__count svg { width: 11px; height: 11px; }

/* "Copy link to this publication" — same button on design-work cards
   (top-left, mirroring .work__count's top-right) and video cards
   (top-right, the only corner not already carrying a badge there). */
.share-btn {
  position: absolute; top: 12px; z-index: 3;
  width: 30px; height: 30px; border-radius: 50%;
  display: grid; place-items: center;
  border: 1px solid rgba(237,239,221,0.22);
  background: rgba(20,22,16,0.6); color: #edefdd; backdrop-filter: blur(8px);
  transition: transform 0.25s var(--ease), background-color 0.25s var(--ease), color 0.25s var(--ease);
}
.work-item .share-btn { left: 12px; }
.vid .share-btn { right: 12px; }
.share-btn svg { width: 14px; height: 14px; }
.share-btn:hover { transform: scale(1.08); }
.share-btn.is-copied { background: var(--accent); color: var(--bg); }

/* Same "copy link" button, but for the lightbox itself — copies a link
   straight to whichever video/work is currently open, not just the grid
   card. Sits at the frame's own bottom-right corner instead of the card's
   top corner, since a video's top-right there would collide with the
   overlay's close button. */
.lightbox__frame .share-btn { top: auto; bottom: 12px; right: 12px; left: auto; }

/* Deep-link landing pulse — scrollToShared() in render.js adds/removes
   this on whichever card matched the page's #hash on load. */
.is-shared { animation: shared-pulse 2s var(--ease); }
@keyframes shared-pulse {
  0%, 100% { box-shadow: none; }
  15%, 55% { box-shadow: 0 0 0 3px var(--accent); }
}
@media (prefers-reduced-motion: reduce) {
  .is-shared { animation: none; box-shadow: 0 0 0 3px var(--accent); }
}

/* ---------- Lightbox ----------
   Two modes on one overlay: an image carousel with prev/next through a
   project's variants, and a large video player — big on every device
   instead of the old approach of just growing the grid card in place. */
.lightbox {
  position: fixed; inset: 0; z-index: 200;
  display: none;
  /* Was place-items:center on the overlay itself, which centres the whole
     modal — fine for a plain image, but the moment a description makes it
     taller than the viewport, centring clips it symmetrically top AND
     bottom with no way to scroll up into the missing part. Scrolling now
     lives on the overlay; .lightbox__inner centres itself via auto margins
     below, which degrades to top-aligned (fully scrollable) the moment it
     no longer fits, instead of clipping. */
  overflow-y: auto;
  padding: clamp(16px, 4vw, 52px);
  background: color-mix(in srgb, var(--bg-deep) 92%, transparent);
  backdrop-filter: blur(18px);
}
.lightbox.is-open { display: block; }

.lightbox__inner {
  display: flex; flex-direction: column; align-items: center;
  max-width: 100%; width: 100%;
  margin-block: auto; /* centred when it fits, top-aligned + scrollable when it doesn't */
}

/* Frame wraps the stage AND its prev/next buttons together, sized to fit
   the image rather than the full overlay width — that's what lets the
   arrows hug the image's own edges instead of sitting pinned way out at
   the viewport edges, far from the picture they control. */
.lightbox__frame {
  position: relative; display: inline-flex; align-items: center;
  max-width: 100%; margin-inline: auto;
}
.lightbox__stage { display: flex; justify-content: center; min-width: 0; }
.lightbox.is-image .lightbox__stage img {
  max-width: 100%; max-height: 74vh; object-fit: contain;
  border-radius: 18px; border: 1px solid var(--line);
}
/* Video mode fills most of the viewport at the right aspect ratio,
   instead of the small fixed-size card the embed used to be squeezed into. */
.lightbox.is-video .lightbox__stage {
  width: min(94vw, 128vh);
  aspect-ratio: 16 / 9;
  position: relative;
  border-radius: 18px; overflow: hidden; border: 1px solid var(--line);
  background: #000;
}
.lightbox.is-video .lightbox__stage iframe {
  position: absolute; inset: 0; width: 100%; height: 100%; border: 0;
}

/* Same timeline-proof + write-up block as the grid card, reused so a
   video's context doesn't disappear the moment it's opened full-size —
   centred and width-capped like .lightbox__desc, since .vid__detail was
   built for a narrow card, not the lightbox's near-full-viewport frame. */
.lightbox__video-detail {
  max-width: 62ch; width: 100%; margin: 18px auto 4px;
  padding: 18px 22px;
  background: var(--card);
  border: 1px solid var(--line-soft);
  border-radius: var(--r-sm, 14px);
}
.lightbox__video-detail .vid__desc { font-size: 14px; }

.lightbox__cap {
  font-family: var(--font-ui); font-size: 11px; letter-spacing: 0.12em;
  color: var(--fg-dim); text-align: center; margin-top: 16px;
}
.lightbox__desc {
  font-family: var(--font-body); font-size: 15px; line-height: 1.7;
  color: var(--fg); text-align: left;
  max-width: 62ch; width: 100%; margin: 18px auto 4px;
  padding: 18px 22px;
  background: var(--card);
  border: 1px solid var(--line-soft);
  border-radius: var(--r-sm, 14px);
}
.lightbox__counter {
  font-family: var(--font-ui); font-size: 10.5px; letter-spacing: 0.14em;
  color: var(--fg-dim); text-align: center; margin-top: 6px;
}

/* button.foo (element+class) outranks .glass (class-only) regardless of
   source order — needed because .glass itself sets position:relative,
   which was silently winning over these buttons' position:absolute at
   equal specificity and collapsing them to their in-flow DOM position. */
button.lightbox__close {
  position: absolute; top: clamp(14px, 3vw, 30px); right: clamp(14px, 3vw, 30px);
  width: 44px; height: 44px; border-radius: 50%;
  display: grid; place-items: center; z-index: 3;
  border: 1px solid var(--line); color: var(--fg);
  background: var(--card);
}
.lightbox__close svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; }

/* Positioned relative to .lightbox__frame (which shrinks to the image's own
   rendered size), just outside its edge — beside the photo, not stranded
   out at the viewport edge regardless of how narrow the image is. */
button.lightbox__nav {
  position: absolute; top: 50%; translate: 0 -50%; z-index: 3;
  flex: 0 0 auto;
  width: 48px; height: 48px; border-radius: 50%;
  display: grid; place-items: center;
  border: 1px solid var(--line); color: var(--fg); background: var(--card);
}
.lightbox__nav svg { width: 18px; height: 18px; }
button.lightbox__nav.lightbox__nav--prev { left: -20px; right: auto; }
button.lightbox__nav.lightbox__nav--next { right: -20px; left: auto; }
@media (max-width: 780px) {
  /* Not enough room to sit outside the frame on narrow viewports — pull
     them just inside the image edge instead of overlapping the chrome. */
  button.lightbox__nav.lightbox__nav--prev { left: 8px; }
  button.lightbox__nav.lightbox__nav--next { right: 8px; }
}
@media (max-width: 620px) {
  .lightbox__nav { width: 40px; height: 40px; }
}

/* Thumbnail strip beneath the main image — click to jump straight to a
   variant instead of only stepping one at a time with prev/next. */
.lightbox__thumbs {
  display: flex; gap: 8px; flex-wrap: wrap; justify-content: center;
  max-width: min(94vw, 720px); margin-top: 16px;
}
.lightbox__thumb {
  width: 56px; height: 56px; border-radius: 10px; overflow: hidden;
  border: 1.5px solid var(--line); padding: 0; flex: 0 0 auto;
  opacity: 0.55; transition: opacity 0.25s var(--ease), border-color 0.25s var(--ease);
}
.lightbox__thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.lightbox__thumb:hover { opacity: 0.85; }
.lightbox__thumb.is-active { opacity: 1; border-color: var(--accent); }
@media (max-width: 620px) {
  .lightbox__thumb { width: 44px; height: 44px; }
}

/* ---------- About page ----------
   The portrait is the section's full background, not a boxed photo beside
   the copy — text sits over it on a scrim so it stays legible edge to edge. */

.about-hero {
  position: relative;
  overflow: hidden;
  min-height: clamp(420px, 62vh, 640px);
  display: flex; align-items: flex-end;
  padding: clamp(28px, 5vw, 56px);
}
.about-hero__bg { position: absolute; inset: 0; z-index: 0; }
.about-hero__bg img {
  width: 100%; height: 100%; object-fit: cover; object-position: 50% 22%;
  filter: saturate(0.92);
  /* Soft memory-like fade: full photo at the centre, dissolving to fully
     transparent toward the edges, instead of a hard rectangular crop. No
     border/background sits behind it (the section isn't .glass anymore),
     so past the fade it's just the page's own background — no visible
     card edge anywhere, the photo simply dissolves into the site. */
  mask-image: radial-gradient(ellipse 82% 84% at 50% 34%, #000 12%, transparent 98%);
  -webkit-mask-image: radial-gradient(ellipse 82% 84% at 50% 34%, #000 12%, transparent 98%);
}
/* Scrim: clear at the top so the photo reads, dense at the bottom where
   the text sits, so the copy never fights the image for legibility. */
.about-hero::after {
  content: ''; position: absolute; inset: 0; z-index: 1;
  background: linear-gradient(0deg,
    color-mix(in srgb, var(--bg-deep) 88%, transparent) 0%,
    color-mix(in srgb, var(--bg-deep) 55%, transparent) 32%,
    color-mix(in srgb, var(--bg-deep) 8%, transparent) 62%,
    transparent 85%);
}

.about-intro {
  position: relative; z-index: 2;
  display: flex; flex-direction: column; gap: 4px;
  color: #edefdd; max-width: 640px;
}
.about-intro .line { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; }
.about-intro .small {
  font-family: var(--font-hand); font-size: clamp(19px, 2.4vw, 28px); color: rgba(237,239,221,0.75);
}
.about-intro .big {
  font-family: var(--font-display); font-weight: 900; font-style: italic;
  font-size: clamp(34px, 5.4vw, 64px); line-height: 1; letter-spacing: -0.03em;
}
.about-lead { font-size: clamp(15px, 1.6vw, 18px); color: rgba(237,239,221,0.82); margin: 20px 0 24px; max-width: 42ch; }
/* A crumb used as a standalone CTA must hug its label, not fill the column. */
.about-intro > .crumb {
  align-self: flex-start;
  border-color: rgba(237,239,221,0.35); color: #edefdd;
  /* Sits on the hero photo, not the page background — the new default
     .crumb fill (tuned for blending into the page) would look like a
     stray light patch here, so it keeps its own dark, photo-safe tint. */
  background: rgba(0,0,0,0.2); box-shadow: none;
}
.about-intro > .crumb:hover { border-color: #edefdd; background: rgba(0,0,0,0.25); }

@media (max-width: 620px) {
  .about-hero { min-height: 78vh; align-items: flex-end; padding: 22px; }
  .about-intro .big { font-size: clamp(28px, 9vw, 44px); }
}

.prose { max-width: 74ch; padding-block: clamp(30px, 5vw, 56px); }
.prose p { font-size: clamp(14.5px, 1.5vw, 16.5px); line-height: 1.85; color: var(--fg-dim); margin: 0 0 26px; }
.prose p:last-child { margin-bottom: 0; }
/* A photo dropped mid-bio — same card treatment (rounded, bordered) as
   every other image on the site, just sized to the prose column instead
   of a grid cell. Natural aspect ratio, not cropped: this is a specific
   candid shot, not a cover image free to be cut to fit a box. */
.prose__photo {
  margin: 4px 0 26px;
  border-radius: var(--r-card);
  overflow: hidden;
  border: 1px solid var(--line-soft);
}
.prose__photo img { width: 100%; display: block; }

/* Opt out of the card frame above for a photo that already carries its
   own soft cut-out (transparent edges fading in from an oval, baked into
   the file) — the rectangular border and rounded corners just boxed in
   the transparent parts, showing as a flat dark corner behind the fade
   instead of the photo dissolving into the page like it's meant to. */
.prose__photo--cutout {
  border: 0;
  border-radius: 0;
  overflow: visible;
}

.tools { display: flex; flex-wrap: wrap; gap: 8px; padding-bottom: clamp(24px, 4vw, 46px); }
.tool {
  font-family: var(--font-ui); font-size: 11px; letter-spacing: 0.1em;
  padding: 9px 16px; border-radius: var(--r-pill);
  border: 1px solid var(--line-soft); color: var(--fg-dim); background: var(--card);
}

/* ---------- Password gate ---------- */

.gate { display: grid; place-items: center; min-height: 62vh; padding-block: 40px; }
.gate__box {
  width: min(100%, 460px); text-align: center;
  background: var(--card); border: 1px solid var(--line-soft);
  border-radius: var(--r-card); padding: clamp(26px, 4vw, 44px);
}
.gate__icon { width: 46px; height: 46px; margin: 0 auto 18px; color: var(--accent-mute); }
.gate__icon svg { width: 100%; height: 100%; fill: none; stroke: currentColor; stroke-width: 1.5; }
.gate__title {
  font-family: var(--font-display); font-weight: 900; font-style: italic;
  font-size: clamp(21px, 2.6vw, 29px); margin: 0 0 10px; letter-spacing: -0.02em;
}
.gate__note { font-size: 14px; color: var(--fg-dim); margin: 0 0 24px; }
.gate__form { display: flex; flex-direction: column; gap: 10px; }
.gate__input {
  font-family: var(--font-body); font-size: 15px;
  padding: 14px 18px; border-radius: var(--r-pill);
  border: 1px solid var(--line); background: var(--bg); color: var(--fg);
  text-align: center; letter-spacing: 0.1em;
  transition: border-color 0.25s var(--ease);
}
.gate__input:focus { outline: none; border-color: var(--accent); }
.gate__submit {
  font-family: var(--font-ui); font-size: 12px; letter-spacing: 0.12em;
  padding: 14px 22px; border-radius: var(--r-pill);
  background: var(--fg); color: var(--bg);
  transition: transform 0.25s var(--ease), opacity 0.25s var(--ease);
}
/* .glass is declared further down this sheet than .gate__submit and both are
   single-class selectors, so its translucent card background used to win —
   leaving the button's dark label on a near-identical dark surface (about
   1.05:1 contrast, i.e. unreadable). Qualifying with .glass takes the
   specificity back and restores the solid pill this was always meant to be.
   Not scoped to a breakpoint: the button was invisible at every width. */
.gate__submit.glass {
  background: var(--fg);
  color: var(--bg);
  border: 1px solid var(--fg);
  backdrop-filter: none; -webkit-backdrop-filter: none;
  font-weight: 600;
}
.gate__submit.glass::before,
.gate__submit.glass::after { content: none; }
.gate__submit:hover { transform: translateY(-1px); opacity: 0.9; }
.gate__err {
  font-size: 13px; color: #d2705f; min-height: 20px; margin: 4px 0 0;
  opacity: 0; transition: opacity 0.2s var(--ease);
}
.gate__err.is-on { opacity: 1; }
.gate__box.is-shaking { animation: shake 0.4s var(--ease); }
@keyframes shake {
  0%,100% { transform: translateX(0); }
  20% { transform: translateX(-9px); }
  40% { transform: translateX(8px); }
  60% { transform: translateX(-5px); }
  80% { transform: translateX(3px); }
}

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

.foot { border-top: 1px solid var(--line-soft); padding-block: clamp(34px, 5vw, 58px) 30px; }
.foot__grid { display: grid; grid-template-columns: 1fr auto; gap: 30px; align-items: start; }
@media (max-width: 720px) { .foot__grid { grid-template-columns: 1fr; } }

.foot__cta-row { display: flex; align-items: center; gap: 22px; margin-bottom: 22px; }
/* A plain transparent-background logo now, not the looping brush-stroke
   video this used to be — that video is opaque (mp4 has no alpha
   channel), and its own baked-in backdrop never quite matched the
   page's, reading as a faint rectangle behind the mark. No card shape
   needed for a transparent image, so border-radius/overflow are gone
   too, not just left harmlessly unused. */
.foot__mark {
  flex: 0 0 auto; width: 108px; border-radius: 20px;
  overflow: hidden; pointer-events: none;
  border: 1px solid var(--line-soft);
}
.foot__mark video { width: 100%; height: auto; display: block; }
.foot__mark .logo-light { display: none; }
:root[data-theme='light'] .foot__mark .logo-dark { display: none; }
:root[data-theme='light'] .foot__mark .logo-light { display: block; }
@media (prefers-color-scheme: light) {
  :root:not([data-theme]) .foot__mark .logo-dark { display: none; }
  :root:not([data-theme]) .foot__mark .logo-light { display: block; }
}
.foot__cta {
  font-family: var(--font-display); font-weight: 800; font-style: italic;
  font-size: clamp(22px, 3.4vw, 40px); line-height: 1.08;
  letter-spacing: -0.025em; margin: 0; max-width: 18ch;
}
.foot__cta .hand { font-family: var(--font-hand); font-style: normal; color: var(--accent); }
@media (max-width: 560px) {
  .foot__cta-row { gap: 14px; }
  .foot__mark { width: 72px; border-radius: 16px; }
}
.foot__meta { display: flex; flex-direction: column; gap: 16px; }
.foot__bottom {
  display: flex; justify-content: space-between; align-items: center; gap: 16px; flex-wrap: wrap;
  margin-top: clamp(30px, 4vw, 48px); padding-top: 22px; border-top: 1px solid var(--line-soft);
  font-size: 11.5px; color: var(--fg-dim);
}
.foot__logo img { height: 26px; opacity: 0.75; }
.foot__logo .logo-light { display: none; }
:root[data-theme='light'] .foot__logo .logo-dark { display: none; }
:root[data-theme='light'] .foot__logo .logo-light { display: block; }
@media (prefers-color-scheme: light) {
  :root:not([data-theme]) .foot__logo .logo-dark { display: none; }
  :root:not([data-theme]) .foot__logo .logo-light { display: block; }
}

/* ---------- Motion ---------- */

/* Word-mask reveal for the homepage hero's name/tagline/quote (see
   heroTextReveal() in motion.js). Each word sits in its own overflow-hidden
   window and slides up from fully behind it — the classic motion-design
   text intro, and the reason it holds up is that the mask boundary always
   falls on a space between words, never mid-letter, so it reads clean even
   through the quote's connected script font. */
.tw-mask { display: inline-block; overflow: hidden; vertical-align: top; }
.tw-word {
  display: inline-block;
  transform: translateY(115%);
  opacity: 0;
  transition: transform 0.52s var(--ease), opacity 0.44s var(--ease);
}
.tw-word.is-in { transform: none; opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .tw-mask { overflow: visible; }
  .tw-word { transform: none; opacity: 1; }
}

/* Scoped to .js so the page still reads fine with scripting disabled.
   Duration used to be 0.22s, which was quick enough that a whole grid
   landing at once read as a stutter rather than an animation — a flicker
   the eye registers as the page hanging. Slower, with the wave-shaped
   per-element delay reveals() computes into --d, so blocks arrive one
   after another instead of all snapping at the same instant. */
.js [data-reveal] {
  opacity: 0;
  transform: translateY(14px);
  transition: opacity 0.48s var(--ease), transform 0.48s var(--ease);
  transition-delay: var(--d, 0ms);
}
/* The crumb buttons are the last thing to arrive on a page (see
   crumbReveal()), one at a time and a second after everything else has
   settled — so they get a slower, taller entrance than the rest of the
   site's, each one clearly its own arrival rather than part of a wave.
   Slower still (0.9s was tried) only blurs them back together: the gap
   between them is fixed, so the longer each fade runs the more of it
   overlaps the next one. transform sits here rather than being left to
   the base rule so the rise is bigger; the .is-in rule below is equally
   specific and comes later, so it still wins the resting state. */
.js [data-reveal][data-reveal-step] {
  transform: translateY(18px);
  transition: opacity 0.75s var(--ease), transform 0.75s var(--ease);
  transition-delay: var(--d, 0ms);
}

.js [data-reveal].is-in { opacity: 1; transform: none; }

/* Every real image/video on the site (portrait, category covers, card
   media, video thumbnails, proof shots, gallery work covers, the about
   page's photos...) gets this instead of the generic rise above: a
   slower blur-into-focus + fade, ~1s, with no vertical movement. transform
   is pinned to none in both states so the generic rule's translateY can't
   sneak in on the same element. Opt in by adding data-reveal-blur
   alongside the existing data-reveal — same trigger (reveals()'s shared
   IntersectionObserver, or renderWorks()'s immediate stagger), different
   look. Needs the extra attribute (not just a selector list of image
   classes) because a handful of media elements — the deck's fanned
   carousel, the lightbox's own open/close — already drive their own
   opacity/transform from JS and would fight this. */
.js [data-reveal][data-reveal-blur] {
  opacity: 0;
  /* 8px, not the 18px this started at. Blur cost climbs steeply with
     radius, and every extra pixel is paid on every frame of the
     transition, on every element playing it at once. 8px still reads
     unmistakably as out-of-focus over a ~1s fade. */
  filter: blur(8px);
  transform: none;
  transition: opacity 0.72s var(--ease), filter 0.72s var(--ease);
  transition-delay: var(--d, 0ms);
}
.js [data-reveal][data-reveal-blur].is-in { opacity: 1; filter: none; }

/* .glass already runs a live backdrop-filter: blur(22px) saturate(1.7).
   Turns out it isn't just an animated filter on that node that's
   expensive — MOVING it is too. Every frame the transform changes, the
   browser has to re-sample whatever's now behind the card's new position
   and re-blur it; a dozen of these translating across the homepage at
   once was still enough to drop frames even after the filter-on-filter
   case below was fixed. So glass panels don't move at all on entrance —
   opacity only, no transform, no filter, regardless of which reveal
   variant (data-reveal-blur, data-reveal-rise) they're also carrying.
   One class more specific than any combination of those, and last in the
   cascade, so it wins outright rather than depending on selector order. */
.js .glass[data-reveal] {
  transform: none !important;
  filter: none !important;
  transition: opacity 0.5s var(--ease) !important;
  transition-delay: var(--d, 0ms) !important;
}
.js .glass[data-reveal].is-in { opacity: 1 !important; }

/* A further variant, layered on top of the blur entrance above: also
   rises up from a small offset below its resting position, for the one
   block that should feel like it's arriving from underneath rather than
   just sharpening into focus in place — currently only the footer's
   contact band. Opt in with data-reveal-rise alongside data-reveal +
   data-reveal-blur (usually paired with data-reveal-late too, so it
   only plays once actually scrolled to). Higher specificity than the
   plain blur rule above (3 attributes vs. 2) wins the transform tug of
   war regardless of source order. */
.js [data-reveal][data-reveal-blur][data-reveal-rise] {
  transform: translateY(32px);
  transition: opacity 0.72s var(--ease), filter 0.72s var(--ease), transform 0.72s var(--ease);
}
.js [data-reveal][data-reveal-blur][data-reveal-rise].is-in { transform: none; }

/* Blinking caret while a [data-typewriter] paragraph (see motion.js) is
   still filling in — purely decorative, so it's gone the instant typing
   finishes rather than left sitting at the end of the sentence. */
.js [data-typewriter].is-typing::after {
  content: '';
  display: inline-block;
  width: 2px;
  height: 0.95em;
  margin-left: 2px;
  background: currentColor;
  vertical-align: -0.15em;
  animation: ar-caret-blink 0.85s steps(1, jump-none) infinite;
}
@keyframes ar-caret-blink { 50% { opacity: 0; } }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
  .js [data-reveal] { opacity: 1; transform: none; }
  .ticker__track { transform: none !important; }
}

/* The About bio's one mention of the studio (WeAreFx) — wrapped into a
   real link by motion.js once the typewriter finishes typing it, not
   present in the markup itself. Underline-only styling, no color shift,
   so it reads as part of the sentence rather than a UI element. */
.bio-link {
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-color: color-mix(in srgb, var(--fg) 35%, transparent);
  transition: text-decoration-color 0.2s var(--ease);
}
.bio-link:hover, .bio-link:focus-visible { text-decoration-color: var(--fg); }

/* Floating live peek of wearefx.studio shown on hover/focus of .bio-link —
   an actual scaled-down iframe (see motion.js), not a static screenshot.
   Built once and left mounted just hidden, so re-hovering never restarts
   the studio site's own intro animation. pointer-events: none throughout:
   it's a peek, not a place to interact — the surrounding link is what
   actually navigates there. */
.bio-preview {
  position: fixed;
  z-index: 90;
  border-radius: 14px;
  overflow: hidden;
  pointer-events: none;
  background: var(--bg-deep);
  border: 1px solid color-mix(in srgb, var(--fg) 14%, transparent);
  box-shadow: 0 24px 48px -16px rgba(0, 0, 0, 0.55);
  opacity: 0;
  transform: translateY(6px) scale(0.97);
  transition: opacity 0.22s var(--ease), transform 0.22s var(--ease);
}
.bio-preview.is-open { opacity: 1; transform: none; }
.bio-preview iframe { border: 0; display: block; transform-origin: top left; }

/* ============================================================
   Liquid Glass
   Apple-style: a translucent pane that samples and refracts what's
   behind it, with a bright specular rim on the top-left edge and a
   soft inner shadow bottom-right. Built from backdrop-filter plus two
   pseudo-elements — no images, no JS, works on every card and pill.
   ============================================================ */

.glass {
  position: relative;
  isolation: isolate;
  /* Tinted enough to read as a distinct surface against the page, not
     just a faint blur that disappears into the background behind it. */
  background: color-mix(in srgb, var(--card-hi) 72%, transparent);
  backdrop-filter: blur(22px) saturate(1.7);
  -webkit-backdrop-filter: blur(22px) saturate(1.7);
  border-color: color-mix(in srgb, var(--fg) 22%, transparent);
  box-shadow: 0 1px 0 0 color-mix(in srgb, var(--fg) 6%, transparent);
}

/* Specular rim — a 1px gradient border via mask, brightest top-left. */
.glass::before {
  content: '';
  position: absolute; inset: 0;
  border-radius: inherit;
  padding: 1px;
  background: linear-gradient(
    145deg,
    color-mix(in srgb, var(--fg) 42%, transparent) 0%,
    color-mix(in srgb, var(--fg) 6%, transparent) 34%,
    transparent 62%,
    color-mix(in srgb, var(--fg) 14%, transparent) 100%
  );
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask-composite: exclude;
  pointer-events: none;
  z-index: 2;
  transition: opacity 0.4s var(--ease);
}

/* Inner depth: light pooling top-left, shadow settling bottom-right. Also
   carries the cursor-follow glow — a soft accent-coloured light that tracks
   the pointer (--mx/--my, set by motion.js), off-canvas by default so it
   never shows until the pointer is actually over the card. */
.glass::after {
  content: '';
  position: absolute; inset: 0;
  border-radius: inherit;
  pointer-events: none;
  z-index: 1;
  background: radial-gradient(
    260px circle at var(--mx, -9999px) var(--my, -9999px),
    color-mix(in srgb, var(--accent) 38%, transparent),
    transparent 72%
  );
  box-shadow:
    inset 1px 1px 1px -0.5px color-mix(in srgb, var(--fg) 30%, transparent),
    inset -1px -1px 1px -0.5px color-mix(in srgb, var(--bg-deep) 60%, transparent),
    inset 0 0 22px 2px color-mix(in srgb, var(--fg) 5%, transparent);
  transition: box-shadow 0.4s var(--ease);
}

a.glass:hover, .glass.is-hoverable:hover, button.glass:hover {
  background: color-mix(in srgb, var(--card) 74%, transparent);
}
a.glass:hover::before, .glass.is-hoverable:hover::before, button.glass:hover::before { opacity: 1; }

/* Card content rides above both glass layers. Deliberately does NOT set
   `position` — doing so overrode `position:absolute` on .card__go and
   flung every arrow button into the text flow. */
.glass > * { z-index: 3; }
.glass > .card__media { z-index: 0; }
.glass > .card__foot { position: relative; z-index: 4; }
.glass > .card__go { z-index: 5; }

/* The pressed/active state on pills reads as the glass compressing. */
button.glass:active, a.glass:active { transform: translateY(1px) scale(0.994); }

@supports not (backdrop-filter: blur(2px)) {
  .glass { background: var(--card); }
}

/* ============================================================
   Hero portrait — steps out from behind the grid
   ============================================================ */

.bento { position: relative; }

/* Placed INTO the hero's own grid cell rather than absolutely against the
   whole grid — that way it can never drift into the rows below when the
   bento reflows. Anchored to the RIGHT, with text on the left of the card.
   The negative-margin breakout overhangs the card's edge without adding
   height to the row. */
/* Pushed as far into the bottom-right corner as possible — flush, with
   only a small deliberate bleed past the rounded edge (not the large
   breakout from before, just enough to read as stepping past the frame
   rather than being boxed a comfortable distance inside it). */
/* Height is locked to the shared grid cell (align-self:stretch + height:100%)
   instead of being derived from width × the photo's own aspect ratio — that
   old approach let the computed height exceed the card's actual rendered
   height whenever the row got shorter (different viewport, font metrics,
   content length), pushing the head above the card's top edge. Locking the
   box's own height and letting object-fit:cover scale+crop the photo to
   fill it means the top can never exceed the frame, on any breakpoint. */
.hero-portrait {
  grid-column: 1 / span 6;
  grid-row: 1 / span 2;
  align-self: stretch;
  justify-self: end;
  width: var(--portrait-w);
  height: 100%;
  overflow: hidden;
  /* Flush to the card's right edge — a right-side bleed read as "cheap"
     per feedback, so only the bottom keeps its small deliberate overhang. */
  margin: 0 0 -8px 0;
  z-index: 25;
  pointer-events: none;   /* never blocks the cards underneath */
  will-change: transform;
}
.hero-portrait img {
  width: 100%; height: 100%; display: block;
  /* contain, not cover: never crop the cut-out — on a narrow/tall cell
     (mobile, where the card is tall from stacked text) cover would zoom
     into a sliver of the photo. contain just scales it down to whatever
     fits and pins it to the bottom-right corner of the cell. */
  object-fit: contain;
  object-position: 100% 100%;
  /* Grounding shadow so the cut-out doesn't look pasted on. */
  filter:
    drop-shadow(0 24px 26px color-mix(in srgb, var(--bg-deep) 50%, transparent))
    drop-shadow(0 4px 10px color-mix(in srgb, var(--bg-deep) 35%, transparent));
}

/* The whole text block — role, name, tagline, quote — reserves a lane
   clear of the portrait's width, so nothing sits behind or under him
   regardless of how tall the figure renders at a given breakpoint. */
.bento { --portrait-w: clamp(210px, 23vw, 320px); }
.c-hero .hero__meta {
  max-width: calc(100% - var(--portrait-w) - var(--pad) * 0.7 - 18px);
}

@media (max-width: 1024px) {
  /* Full-width single row now (was 6 columns / 2 rows). Both c-hero and
     hero-portrait get the SAME explicit single-row placement, so they
     keep sharing one cell exactly as on desktop — switching the portrait
     to position:absolute here was tried and reverted: its nearest
     positioned ancestor is .bento (the whole grid), not this one card,
     so "bottom" measured from the page's total height, not the card's. */
  .bento { --portrait-w: clamp(150px, 26vw, 220px); }
  .bento > .hero-portrait { grid-column: 1 / -1; grid-row: 1; }
}
/* Phone: copy keeps its own lane on the left, the figure fills the right
   one top to bottom. The portrait is a tall cut-out scaled with
   object-fit:contain, so its rendered height follows its column width —
   a wider lane is what makes it stand full height rather than sitting as a
   small crop in the corner. The copy is tightened to match, so the card
   stays roughly as tall as the figure instead of towering over it. */
@media (max-width: 700px) {
  .bento { --portrait-w: 48%; }
  .c-hero { min-height: 0; }
  .c-hero .hero__meta { max-width: calc(100% - var(--portrait-w) - 10px); }
  .c-hero .hero__name { font-size: clamp(23px, 6.6vw, 30px); }
  .c-hero .hero__tagline { font-size: 9.5px; letter-spacing: 0.08em; }
  .c-hero .hero__quote { font-size: 12.5px; line-height: 1.5; margin: 14px 0 0; }
  .bento > .hero-portrait {
    align-self: stretch;
    width: var(--portrait-w);
    height: 100%;
  }
}

/* ============================================================
   Proof shots (Final Cut timelines / client credits)
   ============================================================ */

.vid__meta { padding: 13px var(--pad) 15px; display: flex; flex-direction: column; gap: 11px; }
.vid__meta .vid__cap { padding: 0; }

/* A small square in the corner, not a full-width strip — .vid__meta is a
   column flex container, so without align-self it would stretch to match
   the card's width. */
.proof {
  align-self: flex-start;
  position: relative; display: block;
  width: clamp(88px, 30%, 116px);
  aspect-ratio: 1 / 1;
  border-radius: 10px; overflow: hidden;
  border: 1px solid var(--line-soft);
  background: var(--bg-deep);
  cursor: zoom-in;
  transition: border-color 0.3s var(--ease), transform 0.3s var(--ease);
}
.proof img {
  width: 100%; height: 100%; display: block;
  object-fit: cover; object-position: 50% 74%;
  opacity: 0.68;
  transition: opacity 0.35s var(--ease), transform 0.6s var(--ease);
}
.proof:hover { border-color: var(--accent-mute); }
.proof:hover img { opacity: 1; transform: scale(1.05); }

.proof__hint {
  position: absolute; inset: auto 5px 5px auto;
  display: inline-flex; align-items: center; gap: 4px;
  font-family: var(--font-ui); font-size: 8px; letter-spacing: 0.1em;
  padding: 4px 7px; border-radius: var(--r-pill);
  background: rgba(20, 22, 16, 0.62); color: #edefdd;
  backdrop-filter: blur(8px);
}
.proof__hint svg { width: 10px; height: 10px; }
.proof__hint span { display: none; } /* icon only at this size — no room for the label */

/* The few videos that came with a real write-up on the old site: timeline
   stays put on the left, the text runs beside it on the right. */
.vid__detail {
  display: flex;
  align-items: flex-start;
  gap: 14px;
}
.vid__detail .proof { flex: none; } /* keep its own fixed square size, not the row's stretch */
.vid__desc {
  flex: 1 1 auto;
  min-width: 0;
  font-size: 12.5px;
  line-height: 1.6;
  color: var(--fg-dim);
}

@media (max-width: 560px) {
  /* Not enough room for two columns on a narrow card — stack instead. */
  .vid__detail { flex-direction: column; }
}

/* Instant hover preview — sits above the lightbox (z 200) since it can be
   triggered while a category grid is open behind it. Always pointer-events:
   none so the cursor can never "land on" the enlarged image itself; that's
   what makes leaving the small thumbnail reliably close it, even though
   the preview visually floats well outside the thumbnail's box. */
.proof-peek {
  position: fixed; inset: 0; z-index: 250;
  display: flex; align-items: center; justify-content: center;
  padding: 7vh 7vw;
  background: color-mix(in srgb, var(--bg-deep) 58%, transparent);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  opacity: 0; pointer-events: none;
  transition: opacity 0.2s var(--ease);
}
.proof-peek.is-on { opacity: 1; }
.proof-peek img {
  max-width: min(1100px, 90vw); max-height: 86vh;
  width: auto; height: auto; display: block;
  border-radius: var(--r-card);
  border: 1px solid color-mix(in srgb, #edefdd 32%, transparent);
  box-shadow: 0 34px 80px -20px rgba(0, 0, 0, 0.6);
  object-fit: contain;
  transform: scale(0.96);
  transition: transform 0.25s var(--ease);
}
.proof-peek.is-on img { transform: scale(1); }

@media (hover: none) {
  /* No pointer to hover with on touch — a preview that can only be
     dismissed by "moving the mouse away" would otherwise get stuck open.
     Tap still reaches the full lightbox through the existing click handler. */
  .proof-peek { display: none; }
}
@media (prefers-reduced-motion: reduce) {
  .proof-peek, .proof-peek img { transition: none; }
}

/* ============================================================
   Shuffle deck
   ============================================================ */

/* Full width, single column: heading centred on top, the fan spans the
   whole card below it — was a cramped two-column layout before. */
.deck {
  margin-bottom: clamp(40px, 6vw, 76px);
  padding: clamp(28px, 4vw, 48px) clamp(20px, 3.4vw, 40px);
  display: flex; flex-direction: column; align-items: center;
  gap: clamp(20px, 3vw, 34px); text-align: center;
}

.deck__head { max-width: 56ch; }
.deck__title {
  font-family: var(--font-display); font-weight: 900; font-style: italic;
  font-size: clamp(28px, 4vw, 48px); line-height: 1.02;
  letter-spacing: -0.03em; margin: 4px 0 12px;
}
.deck__note { font-size: 14px; color: var(--fg-dim); margin: 0 auto; max-width: 40ch; }

.deck__body { display: flex; flex-direction: column; gap: 26px; min-width: 0; width: 100%; }

.deck__stage {
  position: relative; width: 100%;
  height: clamp(280px, 34vw, 420px);
  touch-action: pan-y;
}

.deck__card {
  position: absolute; left: 50%; top: 50%;
  width: clamp(230px, 27vw, 360px);
  transition: transform 0.55s var(--ease), opacity 0.45s var(--ease);
  will-change: transform;
}
.deck__hit {
  display: block; width: 100%; position: relative;
  aspect-ratio: 16 / 10;
  border-radius: 18px; overflow: hidden;
  border: 1px solid color-mix(in srgb, var(--fg) 16%, transparent);
  background: var(--bg-deep);
  box-shadow: 0 22px 40px -18px color-mix(in srgb, var(--bg-deep) 90%, transparent);
  cursor: pointer;
}
.deck__hit img { width: 100%; height: 100%; object-fit: cover; }
.deck__hit iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
/* Silent looping preview on the front card — pointer-events off so the
   click that opens the full player still lands on the button underneath;
   a cross-origin iframe would otherwise swallow it. */
.deck__preview { pointer-events: none; }

.deck__play {
  position: absolute; inset: 0; display: grid; place-items: center;
  color: #edefdd; opacity: 0; transition: opacity 0.3s var(--ease);
}
.deck__play svg { width: 40px; height: 40px; filter: drop-shadow(0 2px 8px rgba(0,0,0,0.6)); }
.deck__card.is-front .deck__play { opacity: 0.92; }
.deck__card:not(.is-front) .deck__hit { cursor: pointer; }
.deck__card:not(.is-front) .deck__hit::after {
  content: ''; position: absolute; inset: 0;
  background: color-mix(in srgb, var(--bg-deep) 34%, transparent);
}

/* Caption sits BELOW its card, clear of the fan — not overlapping the
   neighbouring tilted cards the way a tight, small layout would. */
.deck__cap {
  display: block; margin-top: 12px; text-align: center;
  font-family: var(--font-ui); font-size: 10.5px; letter-spacing: 0.1em;
  color: var(--fg-dim); opacity: 0;
  transition: opacity 0.35s var(--ease);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.deck__card.is-front .deck__cap { opacity: 1; }

@media (max-width: 620px) {
  .deck__stage { height: 300px; }
  .deck__card { width: 220px; }
}

.deck__foot { display: flex; align-items: center; justify-content: center; gap: 12px; }
.deck__nav {
  width: 42px; height: 42px; border-radius: 50%;
  display: grid; place-items: center;
  border: 1px solid var(--line); color: var(--fg-dim);
  transition: color 0.25s var(--ease), border-color 0.25s var(--ease);
}
.deck__nav:hover { color: var(--fg); border-color: var(--accent-mute); }
.deck__nav svg { width: 16px; height: 16px; }

.deck__all {
  display: inline-flex; align-items: center; gap: 9px;
  font-family: var(--font-ui); font-size: 11px; letter-spacing: 0.11em;
  padding: 12px 22px; border-radius: var(--r-pill);
  border: 1px solid var(--line); color: var(--fg);
  transition: border-color 0.25s var(--ease), transform 0.25s var(--ease);
}
.deck__all:hover { border-color: var(--accent); transform: translateY(-1px); }
.deck__all svg { width: 14px; height: 14px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }

.profile__handle {
  font-family: var(--font-ui); font-size: 10.5px; letter-spacing: 0.08em;
  color: var(--fg-dim); margin: 5px 0 0;
}
.profile__dot--ig svg { width: 26px; height: 26px; }

.lang-btn .flag { width: 20px; height: 14px; border-radius: 3px; display: block; }
.lang-btn [data-lang-flag] { display: flex; }

/* ============================================================
   Tablet + phone
   ============================================================ */

/* Tablet: two-up grids, roomier tap targets, no hover-only affordances. */
/* Tablet: three across. Fixed column counts rather than auto-fill/minmax —
   a min track width of 300–420px silently collapsed these to one-per-row on
   any portrait tablet, which is the "one giant card per screen" problem. */
@media (max-width: 1024px) {
  :root { --shell: 100%; --gap: 12px; }
  .vid-grid,
  .vid-grid.is-vertical { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .work-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .cat-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }

  /* A card carrying a real write-up (the MAKING OFF pieces) runs much
     taller than a plain video card — sharing a row with one in a fixed
     column grid left a tall description squeezed next to a short, empty
     neighbour. Full-width instead of forcing them into the grid. */
  .vid-grid .vid--has-desc { grid-column: 1 / -1; }
}

/* Phone: two across. One-per-row meant a single work filled the whole
   screen and the page became an endless scroll. */
@media (max-width: 700px) {
  :root { --r-card: 20px; --pad: 16px; }

  .vid-grid,
  .vid-grid.is-vertical { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .work-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .cat-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .vid-grid .vid--has-desc { grid-column: 1 / -1; }

  .cat { aspect-ratio: 4 / 5; }
  .cat--locked { grid-column: 1 / -1; aspect-ratio: 16 / 11; max-height: none; }

  /* 15vw overflowed the screen on the longest Ukrainian headings
     ("НЕПУБЛІЧНЕ ПОРТФОЛІО"), which the body's overflow-x:clip then cut
     off mid-word rather than scrolling to. */
  .page-head__title { font-size: clamp(26px, 9vw, 42px); }

  /* Touch targets: Apple's 44px minimum. */
  .icon-btn { width: 42px; height: 42px; }
  .nav a { padding: 15px 16px; }
  .share-btn { width: 38px; height: 38px; }
  .share-btn svg { width: 16px; height: 16px; }

  .proof img { max-height: 88px; }

  .foot__cta { max-width: 100%; }
  .stats { grid-template-columns: 1fr; }
  .stat { flex-direction: row; align-items: baseline; justify-content: flex-start; gap: 14px; text-align: left; padding: 14px 18px; }
  .stat__lbl { line-height: 1.3; }
}

/* Coarse pointers get no hover-reveal — those states never fire on touch,
   so anything hidden behind :hover would be permanently invisible. */
@media (hover: none) {
  .deck__play { opacity: 0.92; }
  .deck__cap { opacity: 1; }
  .proof img { opacity: 0.9; }
  .card__media { opacity: 0.42; }
}

/* Fullscreen video: strip the letterboxing the card would otherwise add. */
.vid__frame:fullscreen,
.vid__frame:-webkit-full-screen { aspect-ratio: auto; width: 100vw; height: 100vh; }
.vid__frame iframe:fullscreen { width: 100vw; height: 100vh; }

/* ---------- About photo: just the photo ----------
   The source PNG already carries its own soft cut-out (transparent edges),
   so the page must not add anything on top of it. The radial mask faded the
   edges a second time and the ::after scrim laid a dark gradient over the
   bottom — together they read as a black vignette that isn't in the file.
   Both are off here. The image also keeps its natural proportions, so the
   whole frame is visible rather than cover-cropped. */

.about-hero {
  position: relative;
  display: block;
  overflow: visible;
  min-height: 0;
  padding: 0;
}
.about-hero__bg { position: static; inset: auto; }
.about-hero__bg img {
  width: 100%;
  height: auto;            /* natural aspect — no crop, no stretch */
  object-fit: fill;
  object-position: center;
  filter: none;            /* exactly the exported grade */
  mask-image: none;
  -webkit-mask-image: none;
}
.about-hero::after { content: none; }   /* no scrim → no dark vignette */

/* Copy stays over the photo as before; a text shadow carries legibility
   instead of a scrim, so the picture itself is never tinted. */
.about-intro {
  position: absolute;
  z-index: 2;
  left: clamp(20px, 5vw, 56px);
  right: clamp(20px, 5vw, 56px);
  bottom: clamp(18px, 4vw, 48px);
  max-width: 640px;
  color: #edefdd;
  text-shadow: 0 1px 16px rgba(0, 0, 0, 0.6), 0 1px 3px rgba(0, 0, 0, 0.5);
}
.about-intro .small { color: rgba(237, 239, 221, 0.78); }
.about-lead { color: rgba(237, 239, 221, 0.88); }

/* Below this width the 16:9 photo is too short to hold the copy, so the
   text drops underneath it — the photo still shows in full. */
@media (max-width: 760px) {
  .about-intro {
    position: static;
    max-width: none;
    margin-top: clamp(22px, 5vw, 36px);
    color: var(--fg);
    text-shadow: none;
  }
  .about-intro .small { color: var(--fg-dim); }
  .about-lead { color: var(--fg-dim); }
  .about-intro > .crumb { border-color: var(--line); color: var(--fg-dim); }
}

/* ---------- Back-to-top ---------- */

.to-top {
  position: fixed;
  right: clamp(14px, 3vw, 28px);
  bottom: clamp(14px, 3vw, 28px);
  z-index: 120;
  width: 64px; height: 64px;
  border-radius: 50%;
  display: grid; place-items: center;
  /* The glass surface (tint, blur, specular rim) comes from .glass, which
     this button carries; only its size, ink colour and depth are set here. */
  border: 1px solid color-mix(in srgb, var(--fg) 22%, transparent);
  color: var(--fg);
  box-shadow: var(--shadow), 0 1px 0 0 color-mix(in srgb, var(--fg) 6%, transparent);
  opacity: 0; transform: translateY(10px) scale(0.9);
  pointer-events: none;
  transition: opacity 0.3s var(--ease), transform 0.3s var(--ease), background-color 0.3s var(--ease);
}
.to-top.is-on { opacity: 1; transform: none; pointer-events: auto; }
.to-top:hover { transform: translateY(-2px); }
.to-top svg { width: 26px; height: 26px; }

@media (max-width: 620px) {
  .to-top { width: 56px; height: 56px; }
  .to-top svg { width: 23px; height: 23px; }
}
@media (prefers-reduced-motion: reduce) {
  .to-top { transition: opacity 0.2s linear; }
}

/* At the very bottom of the page the fixed button sits right over the
   copyright line — keep that text clear of it wherever the shell is close
   enough to the viewport edge for them to meet. */
@media (max-width: 1340px) {
  .foot__bottom { padding-right: 76px; }
}

/* ============================================================
   Touch layout pass — tablet (≤1024px) and phone (≤700px) only.
   Everything below is inside a max-width query, so the desktop
   layout above is left exactly as it is.
   ============================================================ */

@media (max-width: 1024px) {
  /* --- Header ---------------------------------------------------------
     The language pill and the theme toggle move into the drawer (nav.js
     relocates them at this width). The bar itself keeps only the logo and
     the hamburger, which is what stopped the three controls from piling
     into each other: .lang-btn is an .icon-btn, so the phone rule pinning
     .icon-btn to a 42px square was clipping a pill whose flag + "EN" label
     needs ~62px, and the overflow landed on top of the theme button. */
  .hdr__in { gap: 10px; }
  /* .nav is position:fixed at this width, so it leaves the flex flow and the
     tools cluster collapsed against the logo on the left. Pushing it to the
     right edge puts the menu button under the thumb. */
  .hdr__tools { margin-left: auto; }

  .nav__drawer-tools {
    display: flex; align-items: center; justify-content: flex-end; gap: 10px;
    padding: 4px 16px 12px;
    margin-bottom: 6px;
    border-bottom: 1px solid var(--line-soft);
  }
  .nav__drawer-tools .lang-btn { width: auto; min-width: 62px; }

  /* Drawer scrolls on short/landscape screens rather than running off. */
  .nav { max-height: calc(100dvh - var(--hdr-h)); overflow-y: auto; }
  /* Every row ends at the right edge, on the same side as the button that
     opened the drawer — nothing to reach across the screen for. */
  .nav a { justify-content: flex-end; text-align: right; }

  /* --- Section nav pills ---------------------------------------------
     One pill per row ate a third of the first screen on every subpage. */
  .crumbs { gap: 6px; padding-block: 16px 16px; }
  .crumb { padding: 9px 14px; font-size: 10.5px; letter-spacing: 0.06em; }
  .crumb svg { width: 12px; height: 12px; }

  /* --- Design filters -------------------------------------------------
     Kept on one line as a swipeable strip; stacked full-width pills were
     pushing the actual work four screens down. */
  .filters {
    flex-wrap: nowrap; overflow-x: auto; gap: 6px;
    padding-bottom: 16px;
    scrollbar-width: none; -webkit-overflow-scrolling: touch;
    scroll-snap-type: x proximity;
  }
  .filters::-webkit-scrollbar { display: none; }
  .filter {
    flex: 0 0 auto; scroll-snap-align: start;
    padding: 9px 14px; font-size: 10.5px; letter-spacing: 0.06em;
  }

  /* --- Lightbox -------------------------------------------------------
     The overlay was display:block, so the `margin-block:auto` meant to
     centre the panel resolved to 0 and every image and video opened
     pinned under the status bar — out of thumb reach. Column flex makes
     the auto margins real, and they still collapse to 0 (scrollable from
     the top) when the content is taller than the screen. */
  .lightbox.is-open { display: flex; }
  .lightbox { flex-direction: column; }
  .lightbox__inner { margin-block: auto; }
  .lightbox.is-video .lightbox__stage { width: min(94vw, 128vh); }

  /* Arrows drop below the media instead of floating over it. Overlaid, they
     sat exactly on top of YouTube's own fullscreen and progress controls, so
     a tap meant to enter fullscreen skipped to the next video instead.

     The stage stays in normal flow so its 16/9 aspect-ratio resolves
     against its own final width. Both flex-wrap and grid were tried here
     and both sized the media's row from a width that excluded the buttons'
     tracks and gaps, leaving the video overflowing ~20px down over them.
     The frame instead reserves a strip at the bottom and the two buttons
     are pinned into it, which needs no track sizing at all. */
  .lightbox__frame {
    display: block;
    width: 100%;
    padding-bottom: 62px;
  }
  .lightbox__stage,
  .lightbox.is-video .lightbox__stage { width: 100%; }
  button.lightbox__nav {
    top: auto; bottom: 0;
    /* The base rule centres these with the standalone `translate`
       property, which `transform` does not override — both are cleared. */
    translate: none; transform: none;
    width: 46px; height: 46px;
  }
  button.lightbox__nav.lightbox__nav--prev { left: calc(50% - 53px); right: auto; }
  button.lightbox__nav.lightbox__nav--next { left: calc(50% + 7px); right: auto; }
  button.lightbox__nav:hover { transform: none; }


  /* --- Random-works deck (tablet keeps it, phones hide it below) -------
     Bigger cards and a caption that can actually be read at arm's length,
     always shown rather than waiting for a hover that never comes. */
  .deck__stage { height: 320px; }
  .deck__card { width: 250px; }
  .deck__cap { font-size: 12px; opacity: 1; }
  .deck__title { font-size: clamp(22px, 3.4vw, 30px); }
  .deck__note { font-size: 13px; }
}

@media (max-width: 700px) {
  /* --- Card captions --------------------------------------------------
     Two-up cards leave ~150px per caption; the desktop size wrapped every
     title into a stack of one-word lines. */
  .work__cap, .vid__cap {
    font-size: 9.5px; letter-spacing: 0.05em; line-height: 1.45;
  }
  .work__count { font-size: 9px; padding: 4px 7px; }
  .cat__name { font-size: clamp(13px, 3.6vw, 16px); }
  .cat__watch { font-size: 9px; }
  .cat__count { font-size: 9px; padding: 4px 8px; }
  .locked-note { font-size: 11.5px; }
  .vid__meta { padding: 10px 12px 12px; gap: 8px; }

  /* Description + timeline stack instead of sharing a 150px row. */
  .vid__detail { flex-direction: column; }
  .vid__desc { font-size: 12px; }
  .proof { width: 64px; }

  /* --- Random-works deck ----------------------------------------------
     Hidden on phones: the stacked-card carousel overlaps its own titles at
     this width and there is no room to fan the cards out. The full
     catalogue is one tap away on the design page, and desktop keeps it. */
  section.deck { display: none; }

  /* --- Marquee strip ----------------------------------------------------
     Purely decorative (aria-hidden already) scrolling text band under the
     header. At phone width the words truncate mid-word at both edges with
     no room to read a full phrase, so it reads as noise rather than
     content — dropped here; desktop and tablet keep it. */
  .ticker-strip { display: none; }

  /* --- About ----------------------------------------------------------
     Intro lines were centring into a ragged block once the copy dropped
     below the photo; left-aligned reads as one column again. */
  .about-intro { text-align: left; }
  .about-intro .line { justify-content: flex-start; gap: 8px; }
  .about-intro .big { font-size: clamp(24px, 7.6vw, 36px); line-height: 1.05; }
  .about-intro .small { font-size: 12px; }
  .about-lead { font-size: 14px; margin: 16px 0 20px; }

  /* --- Lightbox chrome ------------------------------------------------ */
  .lightbox { padding: 12px; }
  .lightbox.is-image .lightbox__stage img { max-height: 62vh; }
  .lightbox__desc { font-size: 13.5px; padding: 14px 16px; }
  .lightbox__thumbs { gap: 6px; }

  /* --- Offer card ------------------------------------------------------ */
  .offer__list li { font-size: 12px; gap: 8px; }
  .offer__icon { width: 26px; height: 26px; border-radius: 8px; }
  .offer__icon svg { width: 13px; height: 13px; }
}

/* Only genuinely tiny screens (SE-class, ≤340px) fall back to one column —
   375px is a mainstream iPhone width and keeps the two-up grid. */
@media (max-width: 340px) {
  .vid-grid { grid-template-columns: minmax(0, 1fr); }
}

/* ============================================================
   Custom 3D cursor
   ============================================================
   Palette flips with the theme so the arrow always contrasts with the page:
   light sage body on the dark theme, deep olive on the light one. The rim
   stays near-black in both — it is the shadowed underside of a bevel, which
   doesn't change colour just because the room got brighter. */
:root {
  --cur-hi: #f4f6e9;
  --cur-body: #c1c6b5;
  --cur-shade: #676b5c;
  --cur-rim: #14150f;
  --cur-spec: rgba(255, 255, 255, 0.95);
  --cur-cast: rgba(0, 0, 0, 0.55);
}
:root[data-theme='light'] {
  --cur-hi: #868a75;
  --cur-body: #45483b;
  --cur-shade: #1d2016;
  --cur-spec: rgba(237, 239, 221, 0.75);
  --cur-cast: rgba(28, 30, 25, 0.34);
}
@media (prefers-color-scheme: light) {
  :root:not([data-theme]) {
    --cur-hi: #868a75;
    --cur-body: #45483b;
    --cur-shade: #1d2016;
    --cur-spec: rgba(237, 239, 221, 0.75);
    --cur-cast: rgba(28, 30, 25, 0.34);
  }
}

/* .has-cursor is set by cursor.js, never in the markup — if the script is
   blocked or bails out (touch, no hover), the real cursor is still there.
   !important because several components set their own cursor (zoom-in on
   gallery cards, pointer on the deck) at higher specificity than this. */
html.has-cursor,
html.has-cursor * { cursor: none !important; }
/* Handed back where the native one carries real meaning: a caret to type
   with, and whatever the embedded player wants to show inside its frame. */
html.has-cursor input,
html.has-cursor textarea,
html.has-cursor select { cursor: auto !important; }
html.has-cursor iframe { cursor: auto !important; }

.cursor {
  position: fixed; top: 0; left: 0; z-index: 10000;
  width: 28px; height: 28px;
  pointer-events: none;
  perspective: 320px;
  opacity: 0;
  transition: opacity 0.18s var(--ease);
  will-change: transform;
}
.cursor.is-on { opacity: 1; }
.cursor.is-hidden { opacity: 0; }

/* Rotation pivots on the arrow's point, so the tip stays pinned to where the
   pointer actually is no matter how far the body swings around it. */
.cursor__pivot {
  width: 100%; height: 100%;
  transform-origin: 2px 2px;
  transform-style: preserve-3d;
  transition: scale 0.18s var(--ease);
}
.cursor.is-active .cursor__pivot { scale: 1.22; }
.cursor.is-down .cursor__pivot { scale: 0.88; }

.cursor__svg {
  display: block;
  overflow: visible;
  filter: drop-shadow(1.5px 2.5px 2.5px var(--cur-cast));
}
.cursor__s1 { stop-color: var(--cur-hi); }
.cursor__s2 { stop-color: var(--cur-body); }
.cursor__s3 { stop-color: var(--cur-shade); }
/* paint-order puts the stroke behind the fill, so the rim reads as a bevel
   wrapped around the face rather than a line eating into it. */
.cursor__face {
  stroke: var(--cur-rim);
  stroke-width: 2.9;
  stroke-linejoin: round;
}
.cursor__spec {
  stroke: var(--cur-spec);
  stroke-width: 1.35;
  stroke-linecap: round;
  opacity: 0.9;
}
.cursor.is-active .cursor__spec { opacity: 1; }

/* Belt and braces: a touch device that somehow got the class still shouldn't
   lose its cursor, and a printed page has no pointer at all. */
@media (hover: none), (pointer: coarse), print {
  html.has-cursor,
  html.has-cursor * { cursor: auto !important; }
  .cursor { display: none; }
}

/* ---------- Cross-document view transitions ----------
 * Same-origin navigations only, and silently ignored where unsupported
 * (Firefox, file://, older Safari) — that degrades cleanly to today's
 * instant navigation. Paired names are assigned per-navigation by vt.js. */
@view-transition { navigation: auto; }

::view-transition-group(*) {
  animation-duration: 240ms;
  animation-timing-function: var(--ease);
}
::view-transition-old(root),
::view-transition-new(root) { animation-duration: 200ms; }
/* The morphing category-card/page-head pair travels furthest — a touch
   longer so it doesn't feel clipped. Still well inside a ~300ms budget. */
::view-transition-group(ar-cover) { animation-duration: 320ms; }

/* Navigating between the four main pages (home/about/video/design) plays
   one thing at a time: the page you're leaving fades out completely,
   THEN the one you're arriving at fades in, and only once that's done
   does the new page start its own entrance animations (vt.js holds them
   until the transition finishes — see AR._vtHold there and in motion.js).
   vt.js marks such a navigation with data-vt-fade on <html>; everything
   else keeps the plain 200ms cross-fade above.

   The delay on the incoming half is the whole point: nothing is ever on
   screen twice at once. Two earlier attempts got that wrong and both read
   as the header "bugging" —
     · a directional pan of old and new at the same time, and
     · pinning the header still via its own named transition with
       animation:none, which stacks BOTH captures at full opacity.
   The header islands are translucent (backdrop-filter over a
   part-transparent background), so any moment where two copies coexist
   shows straight through: two active pills lit at once, nav labels
   interleaved into mush, EN and UA side by side. Fading strictly in
   sequence is what actually fixes it — there is no overlap to show
   through. It also hides a second problem for free: the incoming
   snapshot can be captured before the page's own scripts have filled in
   any [data-i18n] text, and with the entrance animations held, none of
   that half-built content is on screen during the fade anyway. */
:root[data-vt-fade]::view-transition-old(root) {
  animation: ar-vt-fade-out 180ms var(--ease) both;
}
:root[data-vt-fade]::view-transition-new(root) {
  animation: ar-vt-fade-in 200ms var(--ease) 180ms both;
}
@keyframes ar-vt-fade-out { to { opacity: 0; } }
@keyframes ar-vt-fade-in { from { opacity: 0; } }

/* The header sits out the fade entirely — it's the same bar on every page
   and should just stay put, not blink off and back on as though the whole
   site reloaded. Naming it captures it separately, so the root fade above
   leaves it alone.
   The important half is that only ONE copy is ever drawn: the outgoing
   capture is held at opacity 0 and the incoming one carries the whole
   transition. An earlier version put animation:none on both, which draws
   BOTH at full opacity — and since the header islands are translucent
   (backdrop-filter over a part-transparent background) the one underneath
   showed straight through: two active pills lit at once, nav labels
   interleaved, EN and UA side by side. One copy can't overlap itself.
   vt.js commits the arriving header visible before the capture is taken,
   since it's a [data-reveal] element whose entrance is being held back.
   Scoped to ≥1025px, where .nav is in normal flow: below that it's a
   position:fixed drawer inside .hdr, and naming an ancestor makes it the
   containing block for fixed descendants for the life of the transition —
   which would reposition an open mobile drawer mid-navigation. Narrow
   viewports keep the plain fade, header included. */
@media (min-width: 1025px) {
  .hdr { view-transition-name: ar-hdr; }
}
::view-transition-old(ar-hdr) { animation: none; opacity: 0; }
::view-transition-new(ar-hdr) { animation: none; opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  /* The ::view-transition pseudo-tree sits outside the document tree, so
     the site's blanket `*, *::before, *::after { transition: none }`
     reduced-motion reset never reaches it — it needs its own rule. vt.js
     also calls skipTransition() as the primary guard; this is the fallback
     in case a future browser runs the CSS transition regardless. */
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) { animation: none !important; }
}

/* ============================================================
   Products — own apps (products.html + products/<id>.html)
   ============================================================ */

.prod-grid {
  display: grid; gap: var(--gap);
  grid-template-columns: repeat(auto-fill, minmax(420px, 1fr));
  padding-bottom: clamp(30px, 5vw, 60px);
}

/* App row: icon, copy, arrow. Deliberately wide rather than square —
   an app is read by its name and one line of purpose, not a cover shot. */
.prod {
  position: relative; display: flex; align-items: center; gap: clamp(16px, 2vw, 22px);
  padding: var(--pad);
  border: 1px solid var(--line-soft); border-radius: var(--r-card);
  background: var(--card); color: inherit; text-decoration: none;
  transition: transform 0.45s var(--ease), border-color 0.45s var(--ease),
              background-color 0.45s var(--ease);
}
.prod:hover { transform: translateY(-4px); border-color: var(--accent-mute); background: var(--card-hi); }

.prod__icon { flex: 0 0 auto; width: clamp(64px, 8vw, 84px); line-height: 0; }
.prod__icon img {
  width: 100%; height: auto; display: block;
  border-radius: 22%;
  box-shadow: var(--shadow);
}
.prod__icon .logo-light { display: none; }
:root[data-theme='light'] .prod__icon .logo-dark { display: none; }
:root[data-theme='light'] .prod__icon .logo-light { display: block; }
@media (prefers-color-scheme: light) {
  :root:not([data-theme]) .prod__icon .logo-dark { display: none; }
  :root:not([data-theme]) .prod__icon .logo-light { display: block; }
}

.prod__body { display: flex; flex-direction: column; gap: 5px; min-width: 0; }
.prod__name {
  font-family: var(--font-display); font-weight: 800; font-style: italic;
  font-size: clamp(21px, 2.4vw, 28px); line-height: 1.1; letter-spacing: -0.02em;
}
.prod__tagline { color: var(--fg-dim); font-size: 14px; line-height: 1.45; }
.prod__tags { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-top: 4px; }

/* "Free" reads as a price tag, so it gets the one filled chip on the card. */
.prod__free {
  font-family: var(--font-ui); font-size: 10px; letter-spacing: 0.12em;
  padding: 5px 11px; border-radius: var(--r-pill);
  background: var(--fg); color: var(--bg);
}
.prod__free--lg { font-size: 11px; padding: 7px 14px; }
.prod__platform {
  font-family: var(--font-ui); font-size: 10px; letter-spacing: 0.12em;
  padding: 5px 11px; border-radius: var(--r-pill);
  border: 1px solid var(--line-soft); color: var(--fg-dim);
}

.prod__go {
  flex: 0 0 auto; margin-left: auto;
  width: 40px; height: 40px; border-radius: 50%;
  display: grid; place-items: center;
  border: 1px solid var(--line-soft); color: var(--fg-dim);
  transition: transform 0.35s var(--ease), color 0.35s var(--ease), border-color 0.35s var(--ease);
}
.prod__go svg { width: 17px; height: 17px; }
.prod:hover .prod__go { transform: translateX(3px); color: var(--fg); border-color: var(--accent-mute); }

@media (max-width: 560px) {
  .prod-grid { grid-template-columns: 1fr; }
  .prod { align-items: flex-start; }
  .prod__go { display: none; }
}

/* ---------- App detail page ---------- */

.app-hero {
  display: flex; align-items: center; gap: clamp(20px, 3vw, 34px);
  padding: clamp(22px, 3vw, 34px) 0 clamp(20px, 3vw, 30px);
  border-bottom: 1px solid var(--line-soft);
}
.app-hero__icon { flex: 0 0 auto; width: clamp(96px, 13vw, 140px); line-height: 0; }
.app-hero__icon img { width: 100%; height: auto; display: block; border-radius: 22%; box-shadow: var(--shadow); }
.app-hero__icon .logo-light { display: none; }
:root[data-theme='light'] .app-hero__icon .logo-dark { display: none; }
:root[data-theme='light'] .app-hero__icon .logo-light { display: block; }
@media (prefers-color-scheme: light) {
  :root:not([data-theme]) .app-hero__icon .logo-dark { display: none; }
  :root:not([data-theme]) .app-hero__icon .logo-light { display: block; }
}

.app-hero__body { min-width: 0; }
.app-hero__name {
  font-family: var(--font-display); font-weight: 900; font-style: italic;
  font-size: clamp(28px, 4.4vw, 52px); line-height: 1.02; letter-spacing: -0.035em;
  margin: 0 0 6px;
}
.app-hero__sub { color: var(--fg-dim); font-size: clamp(14px, 1.6vw, 17px); margin: 0 0 16px; }
.app-hero__actions { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.app-hero__note { font-size: 12px; color: var(--fg-dim); margin: 14px 0 0; max-width: 60ch; line-height: 1.5; }

@media (max-width: 640px) {
  .app-hero { flex-direction: column; align-items: flex-start; }
}

/* Store-style facts strip. */
.app-meta {
  display: grid; gap: 1px;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  background: var(--line-soft);
  border: 1px solid var(--line-soft); border-radius: var(--r-card);
  overflow: hidden;
  margin: clamp(22px, 3vw, 32px) 0;
}
.app-meta__cell {
  display: flex; flex-direction: column; gap: 5px;
  padding: 16px var(--pad); background: var(--card);
}
.app-meta__label { font-family: var(--font-ui); font-size: 9.5px; letter-spacing: 0.14em; color: var(--fg-dim); }
.app-meta__value { font-size: 14px; line-height: 1.35; }

.app-desc { max-width: 76ch; }
.app-desc__lead {
  font-size: clamp(16px, 1.9vw, 19px); line-height: 1.6; margin: 0 0 clamp(22px, 3vw, 30px);
}
.app-sec { margin-bottom: clamp(22px, 3vw, 32px); }
.app-sec__title {
  font-family: var(--font-ui); font-size: 11px; letter-spacing: 0.16em;
  color: var(--fg-dim); margin: 0 0 12px; text-transform: uppercase;
}
.app-sec__p { margin: 0 0 12px; line-height: 1.62; color: var(--fg); font-size: 15px; }
.app-sec__list { margin: 0; padding: 0; list-style: none; display: grid; gap: 9px; }
.app-sec__list li {
  position: relative; padding-left: 20px; font-size: 15px; line-height: 1.55; color: var(--fg);
}
/* Bullet drawn rather than list-style'd, so it lines up with the text
   baseline at every clamp size instead of drifting. */
.app-sec__list li::before {
  content: ''; position: absolute; left: 3px; top: 0.62em;
  width: 5px; height: 5px; border-radius: 50%; background: var(--accent-mute);
}
.app-sec--new { border-top: 1px solid var(--line-soft); padding-top: clamp(22px, 3vw, 30px); max-width: 76ch; }

/* ---------- Screenshot carousel ---------- */

.shots-block { margin-top: clamp(26px, 4vw, 44px); }
.shots__head { display: flex; align-items: center; justify-content: flex-end; gap: 16px; margin-bottom: 14px; }
.shots__ctrl { display: flex; gap: 8px; }
.shots__nav {
  width: 38px; height: 38px; border-radius: 50%;
  display: grid; place-items: center;
  border: 1px solid var(--line-soft); color: var(--fg); background: var(--card);
  transition: opacity 0.3s var(--ease), border-color 0.3s var(--ease), transform 0.3s var(--ease);
}
.shots__nav svg { width: 17px; height: 17px; }
.shots__nav:hover:not(:disabled) { border-color: var(--accent-mute); transform: translateY(-2px); }
.shots__nav:disabled { opacity: 0.32; cursor: default; }

.shots {
  display: flex; gap: var(--gap);
  overflow-x: auto; scroll-snap-type: x mandatory;
  padding-bottom: 6px;
  scrollbar-width: none;
}
.shots::-webkit-scrollbar { display: none; }
/* One screenshot per view. Each slide is a real <button> so it opens the
   shared lightbox (data-gallery) and is reachable by keyboard. */
.shot {
  flex: 0 0 100%; margin: 0; padding: 0; scroll-snap-align: start;
  display: block; cursor: zoom-in; font: inherit; color: inherit;
  border: 1px solid var(--line-soft); border-radius: var(--r-card); overflow: hidden;
  background: var(--card);
  transition: border-color 0.4s var(--ease);
}
.shot:hover { border-color: var(--accent-mute); }
.shot img { width: 100%; height: auto; display: block; }
.shots__count {
  font-family: var(--font-ui); font-size: 11px; letter-spacing: 0.12em;
  color: var(--fg-dim); align-self: center; margin-right: 6px;
}

/* ---------- Download block ---------- */

.app-get {
  margin: clamp(34px, 5vw, 56px) 0 clamp(30px, 5vw, 60px);
  padding: clamp(26px, 4vw, 42px) var(--pad);
  border: 1px solid var(--line-soft); border-radius: var(--r-card);
  background: var(--card); text-align: center;
}
.app-get__title {
  font-family: var(--font-display); font-weight: 800; font-style: italic;
  font-size: clamp(22px, 3.2vw, 34px); line-height: 1.1; letter-spacing: -0.03em;
  margin: 0 0 10px;
}
.app-get__note { font-size: 13px; color: var(--fg-dim); margin: 0 auto 20px; max-width: 56ch; line-height: 1.55; }

.dl-btn {
  display: inline-flex; align-items: center; gap: 10px;
  padding: 12px 22px; border-radius: var(--r-pill);
  font-family: var(--font-ui); font-size: 12px; letter-spacing: 0.1em;
  background: var(--fg); color: var(--bg); text-decoration: none;
  transition: transform 0.35s var(--ease), box-shadow 0.35s var(--ease), opacity 0.35s var(--ease);
  box-shadow: 0 6px 20px -8px color-mix(in srgb, var(--accent) 70%, transparent);
}
.dl-btn svg { width: 17px; height: 17px; flex: 0 0 auto; }
.dl-btn:hover { transform: translateY(-2px); box-shadow: 0 12px 28px -10px color-mix(in srgb, var(--accent) 80%, transparent); }
.dl-btn__meta { opacity: 0.66; font-size: 10px; letter-spacing: 0.1em; }
.dl-btn--big { padding: 16px 30px; font-size: 13px; }
