/* ============================================================
   folio-26 — Vadim Prisyachev

   Ported 1:1 from the Figma frame 24:2 (1440 x 6647).

   Fluid grid. The canvas is 1440px; every spacing and type step
   below is that design value re-expressed as vw with a clamp on
   each end, so the layout stretches instead of sitting in a
   fixed 1440 column. Design px -> vw = px / 14.4.
   ============================================================ */

/* Both faces are served as WOFF2, subset to Latin, Cyrillic and the
   typographic punctuation (349 KB and 119 KB of TTF down to 45 KB and
   5 KB). The TTFs stay in assets/fonts as the source to subset from. */
@font-face {
  font-family: 'Akt';
  src: url('../assets/fonts/Akt-VariableFont_wght.woff2') format('woff2');
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: 'TDAText Condensed';
  src: url('../assets/fonts/TDATextCondensed.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

:root {
  /* the darkest the site goes is #1a1a1a — never pure black */
  --ink: #1a1a1a;
  --ink-60: rgba(26, 26, 26, .6);
  --paper: #ffffff;
  --light: rgba(255, 255, 255, .9);

  --plate: #d0d0d0;   /* the stills that have no export yet */

  /* --- fluid space (design value @1440 in the comment) --- */
  --pad:     clamp(12px,  1.111vw, 26px);     /*  16 */
  --gap:     clamp(16px,  1.667vw, 38px);     /*  24 */
  --gap-sm:  clamp(4px,   0.417vw, 10px);     /*   6 */
  --rhythm:  clamp(64px,  8.333vw, 190px);    /* 120 */

  /* the two column offsets that make the work grid stagger */
  --off-a:   clamp(180px, 41.597vw, 860px);   /*  599 */
  --off-b:   clamp(420px, 102.153vw, 2100px); /* 1471 */

  /* Hero 27:64: lefts are a share of the frame's width (the role blocks
     at 474 and 726 of 1440). The whole stack — two lines of name, then
     one line of the first role and two of the second, all at
     line-height 1 — is centred in the frame's height, so its top is
     half the frame less half the stack. (The canvas had the name at
     227 of 900, a little high of centre.) */
  --hero-name-y: calc(50% - (var(--fs-name) * 2 + var(--fs-role) * 3 + var(--hero-gap)) / 2);
  /* The role's first block stands on the pinned frame's second axis,
     where the language item starts (--hud-axis-2, measured in
     js/main.js); the second keeps the canvas's step of 252 of 1440 to
     the right of it. The percentages are the canvas fallback. The whole
     role is drawn left by --hero-optical, so its first letter's ink meets
     the language item's (js/main.js). */
  --hero-a-x:    calc(var(--hud-axis-2, 32.917%) - var(--hero-optical, 0px));   /* 474 / 1440 on the canvas */
  --hero-b-x:    calc(var(--hero-a-x) + 17.5vw);          /* + 252 / 1440 */

  /* The role hangs off the bottom of the name — two lines of it —
     rather than off its own share of the frame. The name is sized in
     vw and placed in %, so at a short or wide viewport the two drift
     into each other if they are positioned independently. */
  --hero-gap:    0px;
  --hero-role-y: calc(var(--hero-name-y) + var(--fs-name) * 2 + var(--hero-gap));

  --gap-lg:      clamp(24px, 4.444vw, 94px); /* 64 */

  /* --- the screen's own edges ---
     Safari on iOS 26 lays the page out edge to edge — under the status
     bar and under the floating toolbar — and reports what is covered as
     the safe area. The viewport is the whole screen, so anything fixed
     to it (a case, the curtain, the preloader) covers the whole screen;
     what has to stay clear of the hardware is the frame, and it is
     inset by these rather than by --pad alone. --pad stays the grid's
     own measure: it also spaces columns and pulls the full-bleed
     stills out, and adding the screen to it would move all of that.
     Off iOS every one of these is 0 and nothing shifts.
     The foot takes the larger of the two bottom insets: iOS 26 reports
     the floating toolbar's own height in safe-area-max-inset-bottom, so
     the foot row keeps its place instead of sliding under the bar as it
     collapses. Browsers that do not know that name fall back to 0. */
  --safe-t: env(safe-area-inset-top, 0px);
  --safe-r: env(safe-area-inset-right, 0px);
  --safe-b: max(env(safe-area-inset-bottom, 0px), env(safe-area-max-inset-bottom, 0px));
  --safe-l: env(safe-area-inset-left, 0px);

  /* the About block's own vertical steps */
  --ab-table:   clamp(24px,  3.472vw,  72px);  /*  50 — title to the table */
  --ab-media:   clamp(40px, 6.736vw,  140px);  /*  97 */
  --ab-left:    clamp(32px,  5.556vw, 116px);  /*  80 — down from the canvas's 186 */
  --ab-step:    clamp(56px,  8.333vw, 190px);  /* 120 — between the paragraph steps */
  --ab-insp:    clamp(16px, 2.222vw,   46px);  /*  32 */
  --ab-rule:    clamp(24px, 3.194vw,   66px);  /*  46 */
  --ab-talk:    clamp(64px, 17.778vw, 370px);  /* 256 */

  /* --- fluid type --- */
  /* every monospaced run on the page is 14, whatever the canvas says */
  --fs-mono:  clamp(10px, 0.972vw, 20px);   /* 14 */
  --lh-mono:  clamp(13px, 1.250vw, 26px);   /* 18 — one leading for every 14px run */
  --lh-copy:  clamp(16px, 1.667vw, 38px);   /* 24 */
  --fs-body:  clamp(13px, 1.250vw, 30px);   /* 18 */
  --fs-name:  clamp(34px, 8.333vw, 190px);  /* 120 */
  --fs-role:  clamp(24px, 4.444vw, 100px);  /*  64 */
  --fs-title: clamp(38px, 5.556vw, 118px);  /* 80 */
  --fs-proj:  clamp(18px, 1.667vw, 34px);   /* 24 — down from the canvas's 32 */

  --w-copy:   clamp(240px, 31.944vw, 660px);/* 460 */
  --w-para:   clamp(220px, 27.778vw, 580px);/* 400 */

  /* the site runs on two faces only: TDAText Condensed and Akt */
  --cond: 'TDAText Condensed', ui-monospace, 'SFMono-Regular', Menlo, Consolas, monospace;
  --sans: 'Akt', 'Helvetica Neue', Helvetica, Arial, sans-serif;
}

/* ------------------------------ reset ------------------------------ */

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

/* The page is laid out viewport-fit=cover, so it paints to the screen's
   edges and what lies past the document's foot — the strip the home
   indicator stands on, which the browser's bar used to cover — is the
   canvas. The canvas took the body's paper, and the page ends on the
   About block's dark: a white band under the last line. It is given that
   dark instead. The paper stays on the body, which is the document
   itself, so the page reads as it did; only what is beyond it changes. */
html {
  -webkit-text-size-adjust: 100%;
  background: #1a1a1a;
}

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--sans);
  font-weight: 400;
  font-synthesis: none;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow-x: hidden;
  /* every run of text balances its lines, as Figma's Wrap style: Balance.
     It inherits; anything set nowrap keeps its own single line. */
  text-wrap: balance;
}

h1, h2, p { margin: 0; }
img { display: block; max-width: 100%; }
a { color: inherit; text-decoration: none; }

button {
  font: inherit;
  color: inherit;
  /* the UA sheet sets this to none on form controls, which would
     leave the language item the one lowercase word in the frame */
  text-transform: inherit;
  letter-spacing: inherit;
  -webkit-appearance: none;
  appearance: none;
  border-radius: 0;
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
}

/* a see-through blue: the type keeps its own colour, and the tint reads
   on the white paper, the dark About block and the stills alike */
::selection { background: rgba(0, 98, 255, .4); }

.page { position: relative; }

/* ---- preloader (js/preloader.js) ----
   Plain paper and one number in Akt at the size of the hero name,
   travelling along the foot from the left edge to the right as it
   counts from 00 to 66. */
.is-loading,
.is-loading body { overflow: hidden; }

/* js/transition.js — the curtain between the page and a case: a dithered
   dark layer over everything but the grain, with a word typed in its
   middle, set as the header (the condensed cut at 14/18, uppercase) */
.page-transition {
  position: fixed;
  inset: 0;
  z-index: 950;
  display: flex;
  align-items: center;
  padding: var(--pad);
  pointer-events: none;
  visibility: hidden;
}

.page-transition.is-active { visibility: visible; pointer-events: auto; }

/* clearing off the new page: seen, but not in the way */
.page-transition.is-clearing { pointer-events: none; }

.page-transition canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

.page-transition__label {
  --typer-fg: #e5e5e5;
  --typer-bg: #1a1a1a;
  position: relative;
  z-index: 1;
  margin: 0 auto;
  font-family: var(--cond);
  font-weight: 400;
  font-size: var(--fs-mono);
  line-height: var(--lh-mono);
  text-transform: uppercase;
  color: #e5e5e5;
  white-space: nowrap;
}

/* js/scrollbar.js — the site's own scrollbar, after noktt.xyz: on a mouse
   the system bars of the page and of a case are hidden and a thin bar over
   the right edge stands in for them. Square, like the rest of the site; it
   inverts over what it crosses. The line sits flush on the screen's right
   edge; the hit area is 18px wide, reaching in past it. */
.has-scrollbar,
.has-scrollbar body,
.has-scrollbar .case__scroller { scrollbar-width: none; }

.has-scrollbar::-webkit-scrollbar,
.has-scrollbar body::-webkit-scrollbar,
.has-scrollbar .case__scroller::-webkit-scrollbar { display: none; }

.scrollbar {
  position: fixed;
  top: calc(8px + var(--safe-t));
  right: var(--safe-r);
  bottom: calc(8px + var(--safe-b));
  z-index: 900;
  width: 18px;
  mix-blend-mode: difference;
  opacity: 0;
  pointer-events: none;
  transition: opacity .3s;
}

.scrollbar.is-on.is-visible { opacity: 1; pointer-events: auto; }

.scrollbar__thumb {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 48px;
  cursor: grab;
  will-change: transform;
}

.scrollbar__thumb::before {
  content: "";
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  width: 6px;
  /* the frame's plate colour by difference, at 30%: a faint inverse of
     what it crosses */
  background: #e5e5e5;
  opacity: .3;
}

.scrollbar.is-dragging .scrollbar__thumb { cursor: grabbing; }

/* The grain over the whole page, as 2xa.studio runs theirs: a 100px tile
   of faint grey speckle on an oversized fixed layer at 15%, hard-light,
   that jumps to a new offset and quarter-turn ten times every 4s (steps,
   not a slide), so the grain shimmers instead of sitting still. The
   layer is 10rem bigger than the viewport on every side so no jump ever
   shows its edge. Above everything, never in the pointer's way. */
body::after {
  content: '';
  position: fixed;
  top: -10rem;
  left: -10rem;
  width: calc(20rem + max(100vw, 100dvh));
  height: calc(20rem + max(100vw, 100dvh));
  z-index: 1000;
  background: url('../assets/img/noise.png') repeat 50% 50%;
  opacity: .15;
  mix-blend-mode: hard-light;
  pointer-events: none;
  transform: translateZ(0);
  animation: page-grain 4s steps(1) infinite;
}

@keyframes page-grain {
  0%   { transform: translate3d(0, 9rem, 0) rotate(0deg); }
  10%  { transform: translate3d(-1rem, -4rem, 0) rotate(90deg); }
  20%  { transform: translate3d(-8rem, 2rem, 0) rotate(180deg); }
  30%  { transform: translate3d(9rem, -9rem, 0) rotate(90deg); }
  40%  { transform: translate3d(-2rem, 7rem, 0) rotate(0deg); }
  50%  { transform: translate3d(-9rem, -4rem, 0) rotate(90deg); }
  60%  { transform: translate3d(2rem, 6rem, 0) rotate(270deg); }
  70%  { transform: translate3d(7rem, -8rem, 0) rotate(0deg); }
  80%  { transform: translate3d(-9rem, 1rem, 0) rotate(180deg); }
  90%  { transform: translate3d(6rem, -5rem, 0) rotate(270deg); }
  100% { transform: translate3d(-7rem, 0, 0) rotate(360deg); }
}

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

.preloader {
  position: fixed;
  inset: 0;
  z-index: 100;
  background: var(--paper);
  color: #1a1a1a;
  will-change: transform;
}

.is-loaded .preloader { display: none; }

.preloader__count {
  --typer-fg: #1a1a1a;
  --typer-bg: var(--paper);
  position: absolute;
  left: calc(var(--pad) + var(--safe-l));
  bottom: calc(var(--pad) * 2 + var(--safe-b));
  font-family: var(--sans);
  font-weight: 400;
  font-size: var(--fs-name);
  line-height: .8;
  letter-spacing: -.03em;
  font-variant-numeric: tabular-nums;
  will-change: transform;
}

/* ---- text reveals (js/text-reveal.js) ----
   Held hidden from first paint until the text has been split into
   masked lines; the script shows each element as it arms it. */
.tr-wait .hud__item,
.tr-wait .hero__name,
.tr-wait .hero__role,
.tr-wait .project__name,
.tr-wait .project__desc,
.tr-wait .about__title,
.tr-wait .about__jobs,
.tr-wait .about__text,
.tr-wait .about__inspiration > *,
.tr-wait .about__talk,
.tr-wait .about__mail { visibility: hidden; }

/* The line masks are the height of a line box, which at line-height 1
   would shave the descenders off g, p and y while they rise. The masks
   clip with overflow: clip, so the clip can simply be let out past the
   box — overflow-clip-margin takes no part in layout, so nothing
   around the text moves while it is split. (An earlier padding and
   negative-margin version did move it: vertical margins collapse.) */
.tr-line-mask { overflow-clip-margin: .25em; }


/* ============================== HERO ============================== */

/* Plain white, one viewport tall. Nothing sits behind the type. */
.hero {
  position: relative;
  height: 100svh;
  overflow: hidden;
  color: #1a1a1a;
  background: var(--paper);
  z-index: 1;
}

/* js/hero-gallery.js — the cases' pictures drifting along a line through
   the hero, drawn on one canvas under its type */
.hero__gallery {
  position: absolute;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

.hero__name,
.hero__role { z-index: 1; }

/* the name is the condensed cut, not the sans */
.hero__name {
  position: absolute;
  left: var(--pad);
  top: var(--hero-name-y);
  font-family: var(--cond);
  font-weight: 400;
  font-size: var(--fs-name);
  line-height: 1;
  letter-spacing: -.03em;   /* -3.6px at 120 */
  text-transform: uppercase;
  white-space: pre-line;
}

.hero__role {
  position: absolute;
  font-family: var(--sans);
  font-weight: 500;
  font-size: var(--fs-role);
  line-height: 1;
  letter-spacing: -.03em;   /* -1.92px at 64 */
  white-space: pre-line;
}

/* the second block picks up on the next line of the first */
.hero__role--a { left: var(--hero-a-x); top: var(--hero-role-y); }
.hero__role--b { left: var(--hero-b-x); top: calc(var(--hero-role-y) + var(--fs-role)); }

/* ========================== PINNED FRAME ========================== */

/* Figma 28:124 is a single frame the height of the page, with three
   columns that push their own top and bottom items apart. Fixed to
   the viewport it pins the header and the footer together, and
   mix-blend-mode: difference over #e5e5e5 lands on the canvas's own
   #1a1a1a over the paper while coming back near-white over the dark
   About block — the frame is never told what it sits on. */
.hud {
  position: fixed;
  inset: 0;
  z-index: 10;
  padding:
    calc(var(--pad) + var(--safe-t)) calc(var(--pad) + var(--safe-r))
    calc(var(--pad) + var(--safe-b)) calc(var(--pad) + var(--safe-l));
  display: flex;
  align-items: stretch;
  justify-content: space-between;
  gap: var(--pad);
  mix-blend-mode: difference;
  color: #e5e5e5;
  font-family: var(--cond);
  font-size: var(--fs-mono);
  line-height: var(--lh-mono);
  text-transform: uppercase;
  white-space: nowrap;
  pointer-events: none;
}

/* items keep their own width: stretched to the column, the language
   item's plate would span the widest word under it (Петербург) */
.hud__col {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: space-between;
  min-width: 0;
}

.hud__col--end { align-items: flex-end; }

/* The frame's axes are where its columns fall, and the columns are as wide
   as what they hold, so a language whose words are shorter would pull the
   whole grid over. Each column holds the width of its longest word in
   either language — the type is monospaced, one character .452em wide:
   Prisyachev (10) and Русский язык (12); the links read CV in both. */
.hud__col:has(.hud__name) { min-width: calc(10 * .452em); }
.hud__col:has(.hud__lang) { min-width: calc(12 * .452em); }

.hud__row {
  display: flex;
  align-items: center;
  gap: var(--gap-lg);
}

/* the clock and the offset hold the ends of the row above them */
.hud__row--split {
  align-self: stretch;
  justify-content: space-between;
  gap: var(--gap);
}

/* The name's slot (js/header-name.js). The slot keeps the two-line name
   in the frame's flow — so the column holds its width and the other
   columns their places — but shows nothing of it until the travelling
   copy has arrived; it then takes the pointer as the way back to the
   top. The copy is the frame's own type, pinned and inverting with it,
   its two words placed on every scroll frame. */
/* two lines, as in the hero: the first name over the family name */
.hud__name-word {
  position: relative;
  display: block;
  width: fit-content;
  color: transparent;   /* the travelling copy is the type; the slot only keeps its place */
}
.hud__name { pointer-events: none; }
.hud__name.is-docked { pointer-events: auto; }

/* Its hover is the frame's plate, one per line: the first name fills,
   then the family name a beat behind, and they leave in the same
   order. The travelling copy is the frame's first child, so the slot
   paints above it inside the same blend group and the plates invert
   its type exactly as they do any other item's. */
/* (the frame's shared item plate comes later in the file; out-rank it) */
.hud a.hud__name::after { content: none; }

.hud__name-word::after {
  content: '';
  position: absolute;
  inset: 0;
  background: #e5e5e5;
  mix-blend-mode: difference;
  transform: scaleX(0);
  transform-origin: left center;
  animation: hud-fill-out .3s cubic-bezier(0, 1, 0, 1) forwards;
  pointer-events: none;
}

.hud__name-word + .hud__name-word::after { animation-delay: .08s; }

.hud__name.is-docked:hover .hud__name-word::after,
.hud__name.is-docked:focus-visible .hud__name-word::after {
  animation: hud-fill-in .3s cubic-bezier(0, 1, 0, 1) forwards;
}

.hud__name.is-docked:hover .hud__name-word + .hud__name-word::after,
.hud__name.is-docked:focus-visible .hud__name-word + .hud__name-word::after {
  animation-delay: .08s;
}

.hud__fly {
  position: absolute;
  top: 0;
  left: 0;
  pointer-events: none;
}

.hud__fly-word {
  position: absolute;
  top: 0;
  left: 0;
  line-height: 1;
  white-space: nowrap;
  transform-origin: 0 0;
}

/* while the copy stands in for it, the hero's own heading steps aside */
.hero__name.is-flying { opacity: 0; }

/* only the words take the pointer; the frame itself is see-through */
.hud__item { pointer-events: auto; }

/* The footer row rides the viewport until the About block's rule
   reaches it, then docks under the rule and scrolls with the page.
   js/main.js measures the overshoot into --dock; the row is only
   lifted, never laid out again, so the frame's structure holds. */
.hud__foot { transform: translateY(calc(-1 * var(--dock, 0px))); }

/* The hover plate wipes in from the left and leaves to the right.
   It is white, and blends by difference against the label inside the
   frame's own blend group: the label comes out dark where the plate
   covers it, the plate itself comes out as the inverse of the page —
   so the edge of the wipe inverts the word as it crosses it. */
a.hud__item,
.hud__lang {
  position: relative;
  outline: none;
}

/* The wipe is 2xa.studio's menu hover: keyframes rather than a
   transition, so leaving always plays a whole exit from the right, even
   when the entry was cut short; 0.3s on their expo-out, which lands the
   plate almost at once. */
a.hud__item::after,
.hud__lang::after {
  content: '';
  position: absolute;
  inset: 0;
  background: #e5e5e5;   /* by difference: #1a1a1a on the paper, never pure black */
  mix-blend-mode: difference;
  transform: scaleX(0);
  transform-origin: left center;
  animation: hud-fill-out .3s cubic-bezier(0, 1, 0, 1) forwards;
  pointer-events: none;
}

a.hud__item:hover::after,
a.hud__item:focus-visible::after,
.hud__lang:hover::after,
.hud__lang:focus-visible::after {
  animation: hud-fill-in .3s cubic-bezier(0, 1, 0, 1) forwards;
}

/* a plate squeezed to scaleX(0) can still leave a hairline; it is
   hidden outright once it is out */
@keyframes hud-fill-in {
  from { transform-origin: left center; transform: scaleX(0); visibility: visible; }
  to   { transform-origin: left center; transform: scaleX(1); visibility: visible; }
}

@keyframes hud-fill-out {
  from { transform-origin: right center; transform: scaleX(1); visibility: visible; }
  to   { transform-origin: right center; transform: scaleX(0); visibility: hidden; }
}

/* ============================== WORK ============================== */

.work {
  position: relative;
  z-index: 1;
  margin-top: var(--rhythm);
  padding-inline: var(--pad);
}

.project {
  display: flex;
  flex-direction: column;
  gap: var(--gap);
}

/* --ar carries the frame's proportions straight off the canvas.
   The box itself stays transparent so the pixel reveal in
   js/reveal.js tears onto the paper, not onto a grey plate. */
/* 16 between a project's name and its still, tighter than the card's
   24 step — the negative margin takes the difference back off the gap */
.project__media {
  margin-top: calc(var(--pad) - var(--gap));
  position: relative;
  overflow: hidden;
  aspect-ratio: var(--ar);
}

/* only the frames the design leaves empty carry the flat fill */
.project__media:not(:has(img)) { background: var(--plate); }

.project__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* js/reveal.js stands a canvas in front of the still for the 1.6s of
   the pixel reveal, then removes it and hands the frame back. */
.project__media .reveal {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 2;
  pointer-events: none;
}

.project__media.is-masked img { visibility: hidden; }

/* js/cue.js — "Open project details", riding the cursor up and down the
   still, flush to the frame's edges and set as the project names. White through difference, so it inverts the photograph under
   it. It sits above the reveal canvas and never takes the pointer. */
.project__cue {
  --typer-fg: #e5e5e5;
  --typer-bg: #1a1a1a;
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  z-index: 3;
  color: var(--typer-fg);
  mix-blend-mode: difference;
  /* set as the project names above the stills */
  font-family: var(--sans);
  font-weight: 400;
  font-size: var(--fs-proj);
  line-height: 1;
  pointer-events: none;
}

/* one line, justified to both edges — as 2xa's card label, the three
   words are spread across the full width */
.project__cue-label {
  display: block;
  width: 100%;
  white-space: nowrap;
  text-align: justify;
  text-align-last: justify;
}

/* The typer's states, after 2xa's: charInit is not yet typed, the other
   three are what a character flickers through before it settles. */
.typer .char {
  display: inline-block;
  box-sizing: content-box;
  position: relative;
  color: var(--typer-fg);
  background: transparent;
}

.typer .char.charInit { color: transparent; }

/* Blocks and outlines are square-cornered. Neighbouring outlines still
   read as one box rather than a row of cells. */
.typer .char.charFill,
.typer .char.charInverse {
  color: var(--typer-bg);
  background: var(--typer-fg);
}

.typer .char.charBorder::after {
  content: '';
  position: absolute;
  inset: 0;
  border: 1px solid var(--typer-fg);
}

.typer .char.charBorder:has(+ .charBorder)::after { border-right-color: transparent; }
.typer .char.charBorder + .charBorder::after { border-left-color: transparent; }

.project__name {
  font-family: var(--sans);
  font-weight: 400;
  font-size: var(--fs-proj);
  line-height: var(--lh-copy);
  white-space: nowrap;
}

/* pre-line so the copy keeps its designed line break as a plain "\n" */
/* half the card's step between the still and its caption */
.project__desc {
  margin-top: calc(var(--gap) / -2);
  font-family: var(--cond);
  font-size: var(--fs-mono);
  line-height: var(--lh-mono);
  text-transform: uppercase;
  color: var(--ink-60);
  width: var(--w-copy);
  max-width: 100%;
  white-space: pre-line;
}

/* ---- the staggered pair ----
   573 | 811 of the 1408 grid with a 24 gutter. The columns flow
   independently, so each card keeps the vertical offset it has on
   the canvas no matter what the other column is doing. The section is
   laid out from the cases (js/i18n.js), but the offsets are the
   canvas's, by a card's place in its column: the left column opens at
   599 and steps 120, the right opens at 120 and its second card stands
   1471 under the first. A pair follows a full-width card with no gap of
   its own, as on the canvas. What the canvas never had — anything after a
   pair, a full-width card after another — stands a rhythm plus a fifth of
   the screen under it: js/parallax.js drifts each card up to a tenth of
   the screen, so two neighbours closing on each other still keep the rhythm. */
.work__cols + .project,
.work__cols + .work__cols,
.work > .project + .project { margin-top: calc(var(--rhythm) + 20vh); }
.work__cols {
  display: flex;
  align-items: flex-start;
  gap: var(--gap);
}

.work__col {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.work__col--a { flex: 0 0 40.696%; }  /* 573 / 1408 */
.work__col--b { flex: 0 0 57.599%; }  /* 811 / 1408 */

.work__col--a > .project:first-child { margin-top: var(--off-a); }   /*  599 */
.work__col > .project + .project      { margin-top: var(--rhythm); }  /*  120 */
.work__col--b > .project:first-child { margin-top: var(--rhythm); }  /*  120 */
.work__col--b > .project:nth-child(2) { margin-top: var(--off-b); }  /* 1471 */

/* 24:37 is 572 wide inside an 811 column, held to the right edge */
.project--inset {
  width: 70.530%;
  margin-left: auto;
}

/* ============================== ABOUT ============================= */

/* Full-bleed 1440x1499. Three columns — content / gutter / content —
   with the vertical rule living IN the gutter column and spanning
   every row, so the divider tracks the grid at any width. */
.about {
  position: relative;
  z-index: 1;
  margin-top: var(--rhythm);
  padding: var(--pad);
  display: grid;
  grid-template-columns: 36.932% 4.261% 1fr;  /* 520 | 60 | 828 of 1408 */
  align-content: start;
  color: #fff;
  background: #1a1a1a;
  overflow: hidden;
}

/* everything the block holds stacks over its ground */
.about > * {
  position: relative;
  z-index: 1;
}

/* js/dither.js — the ground itself: a drifting Bayer-dithered noise in
   dark grey, one canvas pixel to four CSS px, scaled up with hard edges */
.about > .about__dither {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 0;
  image-rendering: pixelated;
  pointer-events: none;
}

/* set exactly as the hero's role: Akt Medium at 64, -0.03em */
.about__title {
  grid-column: 1 / -1;
  grid-row: 1;
  font-family: var(--sans);
  font-weight: 500;
  font-size: var(--fs-role);
  line-height: 1;
  letter-spacing: -.03em;
  color: var(--light);
}

/* ---- experience table (folio-26 356:439) ----
   Four columns per row: years, company, role, what was done. The last
   two hang on the pinned frame's second and third axes, as the
   paragraphs below do (--axis-2 / --axis-3, measured in js/main.js);
   the first is 201 wide plus its 24 gap. Rows are at least 81 tall,
   24 above and below, split by a 10%-white hairline. The type is set as
   the project captions — the condensed cut at 14, uppercase — every
   column in the same near-white. */
.about__jobs {
  grid-column: 1 / -1;
  grid-row: 2;
  margin: var(--ab-table) 0 0;
  padding: 0;
  list-style: none;
  font-family: var(--cond);
  font-size: var(--fs-mono);
  line-height: var(--lh-mono);
  text-transform: uppercase;
  color: var(--light);
  /* the labels type themselves in on reveal (js/text-reveal.js) */
  --typer-fg: var(--light);
  --typer-bg: #1a1a1a;
}

.about__job {
  display: grid;
  grid-template-columns:
    clamp(150px, 15.625vw, 330px)
    calc(var(--axis-2, 33.1%) - clamp(150px, 15.625vw, 330px))
    calc(var(--axis-3, 69.25%) - var(--axis-2, 33.1%))
    minmax(0, 1fr);
  align-items: start;
  min-height: clamp(56px, 5.625vw, 118px);
  position: relative;
  padding: var(--gap) 0;
}

/* the hairline is drawn rather than bordered, so the reveal can pull it
   out from the left: --rule runs 0 to 1 (js/text-reveal.js), and at
   rest it is simply unset */
.about__job::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1px;
  background: rgba(255, 255, 255, .1);
  transform: scaleX(var(--rule, 1));
  transform-origin: left center;
  pointer-events: none;
}

.about__job:last-child::after { content: none; }

/* the gap to the next column is kept as room a cell may not grow into */
.about__job > * {
  min-width: 0;
  padding-right: var(--gap);
}

/* the duties are set in Akt, in sentence case, at the table's size and
   line — the one column of running text */
.about__job-desc {
  padding-right: 0;
  font-family: var(--sans);
  text-transform: none;
}

/* ---- moodboard carousel (js/wheel.js) ----
   Lifted from noktt.xyz, dropped into the 1392x554 slot Figma 24:50
   leaves for it — the two boxes are the same proportion to within a
   thousandth, so the block stays 1:1 either way. The ring must not
   clip: the tiles are meant to swing past its edges. */
.about__wheel {
  grid-column: 1 / -1;
  grid-row: 3;
  position: relative;
  margin-top: var(--ab-media);
  /* a touch smaller than the 1392 slot, still centred in it; the tiles
     are sized in cqw, so the whole ring scales down with the box */
  width: 85%;
  justify-self: center;
  aspect-ratio: 1392 / 554;
  overflow: visible;
  container-type: inline-size;
  cursor: grab;
  touch-action: pan-y;
  user-select: none;
  -webkit-user-select: none;
}

.about__wheel.is-dragging { cursor: grabbing; }

.wheel__ring {
  position: absolute;
  inset: 0;
}

.wheel__tile {
  position: absolute;
  left: 50%;
  top: 50%;
  overflow: hidden;
  background: var(--plate);
  will-change: transform;
  user-select: none;
  -webkit-user-select: none;
}

/* On noktt the tiles are 168 in a 924-wide ring — 18.2% of it. Our
   slot is 1392, so a fixed 168 would be half as large relative to the
   ring and leave it strung out. Sizing off the container restores the
   original density: the radii fall out of (width - tile) / 2, so the
   whole ellipse comes back to the same proportions. The px pair is
   the fallback where container units are missing. */
.wheel__tile--wide {
  width: 168px;  height: 126px;
  width: 18.182cqw; height: 13.636cqw;
}

.wheel__tile--tall {
  width: 126px;  height: 168px;
  width: 13.636cqw; height: 18.182cqw;
}

.wheel__tile img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  pointer-events: none;
  user-select: none;
  -webkit-user-drag: none;
}

.about__text {
  font-family: var(--sans);
  font-size: var(--fs-body);
  line-height: var(--lh-copy);
  color: #fff;
  width: var(--w-para);
  max-width: 100%;
}

/* The three paragraphs step down the block on the pinned frame's own
   axes: the first under INDEX, the second under the language item, the
   third — with the inspiration row — under open CV. js/main.js measures
   where the frame's columns start into --axis-2 / --axis-3 (relative to
   this grid's content box); the percentages are the 1440 fallback. Each
   step takes its own row, so the stair holds whatever the copy's length. */
.about__text--left {
  grid-column: 1 / -1;
  grid-row: 4;
  margin-top: var(--ab-left);
}

.about__text--mid {
  grid-column: 1 / -1;
  grid-row: 5;
  margin-top: var(--ab-step);
  margin-left: var(--axis-2, 33.1%);
}

.about__right {
  grid-column: 1 / -1;
  grid-row: 6;
  margin-top: var(--ab-step);
  margin-left: var(--axis-3, 69.25%);
}

.about__inspiration {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--gap-sm);
  margin-top: var(--ab-insp);
  font-family: var(--cond);
  font-size: var(--fs-mono);
  line-height: var(--lh-mono);
  text-transform: uppercase;
  color: var(--light);
}

/* cosmos / are.na take the header's hover: no underline, and a plate
   that wipes in from the left and out to the right on the same
   keyframes. Here the plate blends by difference inside the link's own
   isolated group, so over the white label it turns the type black and
   over the dark ground it stays white. */
.chip {
  position: relative;
  isolation: isolate;
  white-space: nowrap;
  outline: none;
}

.chip::after {
  content: '';
  position: absolute;
  inset: 0;
  background: #fff;
  mix-blend-mode: difference;
  transform: scaleX(0);
  transform-origin: left center;
  animation: hud-fill-out .3s cubic-bezier(0, 1, 0, 1) forwards;
  pointer-events: none;
}

.chip:hover::after,
.chip:focus-visible::after {
  animation: hud-fill-in .3s cubic-bezier(0, 1, 0, 1) forwards;
}

.about__rule--h {
  grid-column: 1 / -1;
  grid-row: 7;
  height: 1px;
  margin-top: var(--ab-rule);
  background: rgba(255, 255, 255, .1);   /* the table's hairline */
}

/* "let's talk" sits at the far left, on the e-mail's baseline; the
   row keeps its old top, so the address stays exactly where it was */
.about__contact,
.about__talk {
  grid-row: 8;
  margin-top: calc(var(--ab-talk) + var(--lh-mono));
  align-self: last baseline;
}

/* the address hangs on the second axis, under the second paragraph */
.about__contact {
  grid-column: 1 / -1;
  margin-left: var(--axis-2, 33.1%);
}

.about__talk {
  grid-column: 1;
  font-family: var(--cond);
  font-size: var(--fs-mono);
  line-height: var(--lh-mono);
  text-transform: uppercase;
  color: #fff;
}

/* the address types itself over on hover, as the card cue does */
.about__mail {
  --typer-fg: var(--light);
  --typer-bg: #1a1a1a;
  display: inline-block;
  font-family: var(--sans);
  font-size: var(--fs-title);
  line-height: 1;
  letter-spacing: -.01em;
  color: var(--light);
  white-space: nowrap;
}

/* =============================== CASE ============================== */

/* js/case.js — a project page in a modal, at /case/<id>/. It covers the
   page under the pinned frame (which stays on top and keeps inverting
   over it), scrolls in its own layer, and freezes the page beneath. */
.case-open,
.case-open body { overflow: hidden; }

/* A belt to the safe area's braces. iOS lays a fixed element out against
   the rect the browser's own bars leave unobscured, while the document
   under it paints to the screen's edges — so if a case (fixed, inset 0)
   is ever held to that rect, the page shows past it at both ends. The
   ground a case stands on is therefore laid on the document as well:
   the page goes out of sight and the canvas takes the case's own dark,
   so whatever is painted outside the case is the same #1a1a1a. Held off
   while the preloader is up, which is paper: a cold /case/<id>/ mounts
   the case under it. */
.case-open:not(.is-loading),
.case-open:not(.is-loading) body { background: #1a1a1a; }

.case-open #smooth-wrapper { visibility: hidden; }

/* the frame ends where the case's scrollbar begins */
.case-open .hud { right: var(--case-sbw, 0px); }

.case {
  /* inside the text column the site's grid has one more line, the
     frame's third axis: --col-a is the track from the column's start
     (the second axis) up to it, less the gap, so whatever follows it
     starts on that axis */
  --col-a: calc(var(--hud-axis-3, 69.25%) - var(--hud-axis-2, 33.1%) - var(--gap));
  /* case pages are dark: the page's ink turned light, its rules and tints
     re-based on white */
  --ink: rgba(255, 255, 255, .9);
  --ink-60: rgba(255, 255, 255, .55);
  --case-rule: rgba(255, 255, 255, .1);
  --case-tint: rgba(255, 255, 255, .06);
  --plate: #262626;
  position: fixed;
  inset: 0;
  z-index: 9;
  background: #1a1a1a;
  color: var(--ink);
  /* light type on the dark ground reads heavier than the same cut on
     paper: running text is set a step lighter than the page's 400
     (headings and emphasis keep their own 500) */
  font-weight: 340;
  /* running text in a case: 18 on 24 */
  --case-fs: 18px;
  --case-lh: 24px;
}

.case:focus { outline: none; }

.case__scroller {
  position: absolute;
  inset: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
}

/* no card cue left hanging on the page behind an open case */
.case-open #smooth-wrapper .project__cue { visibility: hidden; }

/* ---- cover, on the site's grid: the photograph in the first column, from
   the left edge to the frame's second axis, at its own proportions —
   nothing cropped — beside the text, both starting one gap under the
   frame's two-line top-left slot. js/case.js spans it down the rows it
   stands beside. ---- */
.case__cover {
  display: grid;
  grid-column: full-start / text-start;
  grid-row: 1;
  align-self: start;
  /* reaches out over the body's left padding, so the photograph can meet
     the screen's edge while the record keeps to the margin */
  margin: 0 0 0 calc(var(--pad) * -1);
  padding-left: var(--pad);
  overflow: hidden;
}

/* the photograph takes the first half of the column, out to the screen's
   left edge */
.case__cover-img {
  grid-area: 1 / 1;
  display: block;
  margin-left: calc(var(--pad) * -1);
  width: calc((100% - var(--gap)) / 2 + var(--pad));
  height: auto;
  background: var(--plate);
}

/* ---- the record, on the cover, in three steps like the About text: three
   equal columns across the whole first column, each one About step lower
   than the one before, flush with the cover's left edge and one gap under
   its top. In the frame's
   type, inverting over the photograph like the frame does; the steps
   drift apart as the case scrolls (js/case.js). ---- */
.case__record {
  grid-area: 1 / 1;
  align-self: start;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  column-gap: var(--gap);
  padding: var(--gap) 0;
  font-family: var(--cond);
  font-size: var(--fs-mono);
  line-height: var(--lh-mono);
  text-transform: uppercase;
  color: #e5e5e5;
  mix-blend-mode: difference;
  pointer-events: none;
}

.case__record-step {
  margin: 0;
  display: flex;
  flex-direction: column;
  row-gap: var(--lh-mono);
  min-width: 0;
  will-change: transform;
}

.case__record-step { align-self: start; }
.case__record-step:nth-child(2) { margin-top: var(--ab-step); }
.case__record-step:nth-child(3) { margin-top: calc(var(--ab-step) * 2); }

.case__record-item { min-width: 0; }
.case__record dt { opacity: .6; }
.case__record dd { margin: 0; text-wrap: balance; }

/* ---- the body, on the site's grid: the text column starts on the
   frame's second axis (where the language item, the hero's role and
   the table's role column start) and is half the screen wide; full
   images run edge to edge, wide ones from that axis to the right
   edge ---- */
/* clips the eased body (js/case.js) so its lag adds nothing to the scroll */
.case__track { overflow: clip; }

/* smoothed: the track holds still at the top of the layer for one screen,
   the body moves inside it by transform, and a spacer after it gives the
   layer the body's scroll length */
.case__scroller.is-smooth .case__track {
  position: sticky;
  top: 0;
  height: 100%;
}

.case__body {
  display: grid;
  grid-template-columns:
    [full-start] calc(var(--hud-axis-2, 33.1%) - var(--pad) - var(--gap))
    [text-start] minmax(0, 50vw)
    [text-end] minmax(0, 1fr) [full-end];
  column-gap: var(--gap);
  padding:
    calc(var(--pad) + var(--safe-t) + var(--lh-mono) * 2 + var(--gap))
    calc(var(--pad) + var(--safe-r))
    calc(var(--rhythm) * 1.5)
    calc(var(--pad) + var(--safe-l));
}

.case__head,
.case__article > * { grid-column: text; }

.case__article { display: contents; }

.case__title {
  font-family: var(--sans);
  font-weight: 500;
  font-size: var(--fs-role);
  line-height: 1;
  letter-spacing: -.03em;
}

.case__intro {
  margin-top: var(--gap);
  font-size: var(--fs-proj);
  line-height: 1.35;
  letter-spacing: -.01em;
}

/* ---- the rail: close and copy link, pinned in the first column under
   the name, in the frame's type, inverting over whatever it crosses ---- */
/* close and copy link sit in the frame's top-left slot, on the name's two
   lines; the name steps out of the way while a case is open */
.case-open .hud__fly,
.case-open .hud .hud__name,
.case-open .hud .hud__name.is-docked {
  visibility: hidden;
  pointer-events: none;
}

.case__rail {
  position: fixed;
  z-index: 1;
  left: calc(var(--pad) + var(--safe-l));
  top: calc(var(--pad) + var(--safe-t));
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  font-family: var(--cond);
  font-size: var(--fs-mono);
  line-height: var(--lh-mono);
  text-transform: uppercase;
  color: #e5e5e5;
  mix-blend-mode: difference;
}

.case__tool {
  position: relative;
  color: inherit;
  outline: none;
}

.case__tool::after {
  content: '';
  position: absolute;
  inset: 0;
  background: #e5e5e5;
  mix-blend-mode: difference;
  transform: scaleX(0);
  transform-origin: left center;
  animation: hud-fill-out .3s cubic-bezier(0, 1, 0, 1) forwards;
  pointer-events: none;
}

.case__tool:hover::after,
.case__tool:focus-visible::after {
  animation: hud-fill-in .3s cubic-bezier(0, 1, 0, 1) forwards;
}

/* ---- blocks ---- */
.case__article > :first-child { margin-top: var(--rhythm); }

.cs-section { margin-top: var(--rhythm); }

.cs-label,
.cs-caption {
  display: block;
  font-family: var(--cond);
  font-size: var(--fs-mono);
  line-height: var(--lh-mono);
  text-transform: uppercase;
  color: var(--ink-60);
}

.cs-section__title {
  margin-top: 0;
  font-family: var(--sans);
  font-weight: 500;
  font-size: clamp(22px, 2.5vw, 52px);
  line-height: 1.1;
  letter-spacing: -.02em;
}

.cs-lead {
  margin-top: var(--gap);
  font-size: var(--fs-proj);
  line-height: 1.35;
  letter-spacing: -.01em;
}

/* running text keeps to a reading measure — about seventy characters —
   inside the half-screen column; headings may use the whole column */
.cs-p,
.cs-lead,
.cs-callout,
.case__intro { max-width: 36em; }

.cs-p {
  margin-top: calc(var(--gap) * .75);
  font-size: var(--case-fs);
  line-height: var(--case-lh);
}

.cs-lead + .cs-p,
.cs-section + .cs-p { margin-top: var(--gap); }

.cs-p em,
.cs-lead em,
.cs-quote em { font-style: italic; }

.case strong { font-weight: 500; }

/* a link in a case's copy: underlined, in the copy's own colour */
.case .cs-link {
  color: inherit;
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: .18em;
}

.case .cs-link:hover { text-decoration-color: transparent; }

/* a highlight is outlined, not filled: a white hairline with 4px corners
   (an inset shadow, so it adds nothing to the line's height), a little
   room inside and out, closed on each line it wraps across */
.case mark {
  padding: .05em .25em;
  margin: 0 .05em;
  border-radius: 4px;
  background: none;
  box-shadow: inset 0 0 0 1px #fff;
  color: inherit;
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone;
}

.cs-sub + .cs-p { margin-top: calc(var(--gap) * .5); }

.cs-sub {
  margin-top: calc(var(--gap) * 2);
  font-size: var(--fs-proj);
  font-weight: 500;
  line-height: 1.2;
  letter-spacing: -.01em;
}

/* a note set off by a rule down its left edge, nothing more */
.cs-callout {
  margin-top: calc(var(--gap) * 1.5);
  padding-left: var(--gap);
  border-left: 1px solid var(--ink);
  font-size: var(--case-fs);
  line-height: var(--case-lh);
}

.cs-list { margin-top: calc(var(--gap) * 1.5); }

.cs-list ul,
.cs-list ol {
  margin: var(--gap-sm) 0 0;
  padding: 0;
  list-style: none;
}

/* the item first, its number at the right end of the row */
.cs-list li {
  display: grid;
  grid-template-columns: minmax(0, 1fr) calc(var(--gap) * 1.5);
  column-gap: var(--gap);
  padding: calc(var(--gap) * .5) 0;
  border-top: 1px solid var(--case-rule);
  font-size: var(--case-fs);
  line-height: var(--case-lh);
}

.cs-list__n {
  order: 2;
  text-align: right;
  font-family: var(--cond);
  font-size: var(--fs-mono);
  line-height: var(--case-lh);
  color: var(--ink-60);
}

.cs-table { margin-top: calc(var(--gap) * 1.5); }

/* a table's columns are all the same width */
.cs-table__row {
  display: grid;
  grid-template-columns: repeat(var(--cols, 2), minmax(0, 1fr));
  column-gap: var(--gap);
  padding: calc(var(--gap) * .5) 0;
  border-top: 1px solid var(--case-rule);
  font-size: var(--case-fs);
  line-height: var(--case-lh);
}

.cs-table__row--head {
  border-top: 0;
  font-family: var(--cond);
  font-size: var(--fs-mono);
  line-height: var(--lh-mono);
  text-transform: uppercase;
  color: var(--ink-60);
}

.cs-media { margin: calc(var(--gap) * 2) 0 0; }

.cs-media img {
  display: block;
  width: 100%;
  height: auto;
  background: var(--plate);
}

/* full and wide pictures (galleries too) run out to the screen's right
   edge, past the body's padding */
.cs-media--full { grid-column: full !important; }

.cs-media--wide { grid-column: text-start / full-end !important; }

.cs-media--full,
.cs-media--wide { margin-right: calc(var(--pad) * -1); }

/* a full picture meets the left edge of the screen too */
.cs-media--full { margin-left: calc(var(--pad) * -1); }

/* a gallery's second image starts on the third axis */
.cs-gallery__grid {
  display: grid;
  grid-template-columns: repeat(var(--cols, 2), minmax(0, 1fr));
  gap: var(--gap);
}

.cs-gallery[style*="--cols:2"] .cs-gallery__grid { grid-template-columns: var(--col-a) minmax(0, 1fr); }

.cs-gallery__grid img { aspect-ratio: 4 / 3; object-fit: cover; }

.cs-quote {
  margin: calc(var(--gap) * 2.5) 0 0;
  padding: 0;
}

.cs-quote p {
  font-weight: 500;
  font-size: clamp(22px, 2.5vw, 52px);
  line-height: 1.15;
  letter-spacing: -.02em;
}

.cs-quote cite {
  display: block;
  margin-top: var(--gap);
  font-style: normal;
  font-family: var(--cond);
  font-size: var(--fs-mono);
  line-height: var(--lh-mono);
  text-transform: uppercase;
  color: var(--ink-60);
}

.cs-metrics {
  margin-top: calc(var(--gap) * 2);
  display: grid;
  grid-template-columns: var(--col-a) minmax(0, 1fr);
  column-gap: var(--gap);
}

.cs-metric {
  display: flex;
  flex-direction: column;
  gap: var(--gap-sm);
  padding: var(--gap) 0;
  border-top: 1px solid var(--case-rule);
}

.cs-metric__value {
  font-weight: 500;
  font-size: var(--fs-role);
  line-height: 1;
  letter-spacing: -.03em;
}

.cs-metric__label {
  font-family: var(--cond);
  font-size: var(--fs-mono);
  line-height: var(--lh-mono);
  text-transform: uppercase;
  color: var(--ink-60);
}

.cs-gantt { margin-top: calc(var(--gap) * 1.5); }

/* label up to the third axis, the bar from it, the dates at the end */
.cs-gantt__row {
  display: grid;
  grid-template-columns: var(--col-a) minmax(0, 1fr) clamp(96px, 9vw, 190px);
  column-gap: var(--gap);
  align-items: center;
  padding: calc(var(--gap) * .4) 0;
  border-top: 1px solid var(--case-rule);
  font-family: var(--cond);
  font-size: var(--fs-mono);
  line-height: var(--lh-mono);
  text-transform: uppercase;
}

.cs-gantt__track {
  position: relative;
  height: var(--lh-mono);
  background: var(--case-tint);
}

.cs-gantt__bar {
  position: absolute;
  top: 0;
  bottom: 0;
  background: var(--ink);
}

.cs-gantt__dates { color: var(--ink-60); text-align: right; }

/* ---- other projects: one card on each of the frame's three axes ---- */
.case__more {
  grid-column: full;
  margin-top: calc(var(--rhythm) * 1.5);
}

/* ---- the end: the room at the foot of the case; scrolling through it
   brings the frame's top row, Close and Copy link with it, down to the
   middle of the screen (--case-drop, js/case.js). Its height is set so that
   there the row's top sits exactly one rhythm under the foot of Other
   projects, whatever the case or the screen: the row lands at
   50vh − one line, the projects end at 100vh − the body's foot padding −
   this room. ---- */
.case__end {
  grid-column: full;
  height: calc(50vh + var(--lh-mono) + var(--rhythm) - var(--rhythm) * 1.5);
}

.case-open .hud__lang,
.case-open .hud__col--end > .hud__row:not(.hud__foot),
.case__rail { transform: translateY(var(--case-drop, 0px)); }

.case__more-title {
  font-family: var(--cond);
  font-weight: 400;
  font-size: var(--fs-mono);
  line-height: var(--lh-mono);
  text-transform: uppercase;
  color: var(--ink-60);
}

.case__more-grid {
  margin-top: var(--gap);
  display: grid;
  grid-template-columns:
    calc(var(--hud-axis-2, 33.1%) - var(--pad) - var(--gap))
    calc(var(--hud-axis-3, 69.25%) - var(--hud-axis-2, 33.1%) - var(--gap))
    minmax(0, 1fr);
  gap: var(--gap);
}

/* the cards are the page's own (.project): name, still, caption — here
   with the whole caption, and the cue on hover */
.case__more-grid { align-items: start; }

.case .project__desc {
  width: auto;
  white-space: normal;
}

/* blocks rise in as they reach the screen, on the site's easing */
.cs-rise {
  opacity: 0;
  transform: translateY(16px);
  transition: opacity 1.1s cubic-bezier(.16, 1, .3, 1), transform 1.1s cubic-bezier(.16, 1, .3, 1);
}

.cs-rise.is-in { opacity: 1; transform: none; }

/* the cards on the page open their case */
[data-case] .project__media { cursor: pointer; }

@media (max-width: 900px) {
  .case__body { grid-template-columns: [full-start text-start] minmax(0, 1fr) [text-end full-end]; }
  /* on a phone the cover heads the column, the text under it */
  .case__cover { grid-column: full; grid-row: auto !important; margin-bottom: calc(var(--rhythm) * .75); }
  /* a phone lays the record out under the cover, two to a row, still */
  .case__cover { display: block; overflow: visible; margin-left: 0; padding-left: 0; }
  /* full width here, so it meets both edges of the screen */
  .case__cover-img {
    width: calc(100% + var(--pad) * 2);
    max-width: none;
    margin-inline: calc(var(--pad) * -1);
  }
  /* the record still steps, two columns wide, zigzagging down: the first
     step rides up onto the foot of the photograph (inverting over it as
     on a desktop), the second a step lower on the right, the third under
     the first */
  .case__record {
    position: relative;
    margin-top: calc(var(--rhythm) * -1);
    grid-template-columns: repeat(2, minmax(0, 1fr));
    padding: 0;
  }
  /* running text in a case: 16 on 24 on a phone */
  .case { --case-fs: 16px; --case-lh: 24px; }
  /* tables set tighter still, 14 on 20 */
  .cs-table__row:not(.cs-table__row--head) { font-size: 14px; line-height: 20px; }
  .case__record-step:nth-child(1) { grid-column: 1; margin-top: 0; }
  .case__record-step:nth-child(2) { grid-column: 2; margin-top: var(--ab-step); }
  .case__record-step:nth-child(3) { grid-column: 1; margin-top: var(--lh-mono); }
  .cs-metrics,
  .cs-gallery[style*="--cols:2"] .cs-gallery__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .cs-gantt__row { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
  .cs-gantt__dates { grid-column: 1 / -1; text-align: left; }
  .case__more-grid { grid-template-columns: minmax(0, 1fr); }
}

/* ============================ responsive ========================== */

/* The canvas is a desktop composition: below it the two work columns
   stop overlapping and the About grid collapses to one column. */
@media (max-width: 1000px) {
  .about { grid-template-columns: 1fr; }

  .about__title,
  .about__jobs,
  .about__wheel,
  .about__text--left,
  .about__text--mid,
  .about__right,
  .about__rule--h,
  .about__talk,
  .about__contact {
    grid-column: 1;
    grid-row: auto;
  }

  .about__text--left { margin-top: var(--rhythm); }
  .about__text--mid,
  .about__right { margin-top: var(--gap); margin-left: 0; }
  /* the middle paragraph keeps its step: it goes over to the right edge */
  .about__text--mid { margin-left: auto; }
  .about__talk { margin-top: var(--rhythm); align-self: auto; }
  .about__contact { margin-top: 0; margin-left: 0; align-self: auto; }

  .about__mail { white-space: normal; word-break: break-word; }

  /* years and company on one line, role and description under them */
  .about__job { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); row-gap: 4px; }
  .about__job-desc { grid-column: 1 / -1; }
}

/* ---- the About block on a phone ----
   Its steps are fluid, drawn for the 1440 canvas, and this narrow every
   one of them sits on the floor of its clamp: the table opens 24 under
   the title, the paragraphs stand 16 apart, the rule 24 under them. The
   stair that carries the block on a desktop is gone here — the columns
   are stacked — so those floors leave one wall of type with no room in
   it. The steps are set again off the rhythm, which is the page's own
   measure, rather than off what the clamps happen to bottom out at, and
   the block is given a foot and a head of its own: it is the only thing
   on the screen that is dark, and it should read as a held breath
   rather than a column that ran out of margin. Nothing moves above
   900 — the stair is still there to do this work. */
@media (max-width: 900px) {
  .about {
    margin-top: calc(var(--rhythm) * 1.5);
    padding: calc(var(--rhythm) * .75) var(--pad);
  }

  /* the table opens well clear of the title, and its rows part */
  .about__jobs { margin-top: calc(var(--rhythm) * .75); }
  .about__job { padding: var(--gap-lg) 0; }

  .about__wheel { margin-top: var(--rhythm); }

  /* the three paragraphs: a step between them, not a line's leading */
  .about__text--mid,
  .about__right { margin-top: calc(var(--rhythm) * .75); }

  .about__inspiration { margin-top: var(--gap-lg); }

  /* the rule, and the address under it, close the page */
  .about__rule--h { margin-top: var(--rhythm); }
  .about__talk { margin-top: calc(var(--rhythm) * 1.25); }
}

@media (max-width: 900px) {
  /* the stepped hero has nowhere to go this narrow: it stacks, and
     the padding keeps it clear of the pinned frame at both ends */
  .hero {
    height: auto;
    min-height: 100svh;
    display: flex;
    flex-direction: column;
    /* the type sits low, under the stream of pictures (js/hero-gallery.js) */
    justify-content: flex-end;
    padding:
      calc(var(--rhythm) * 1.4 + var(--safe-t)) calc(var(--pad) + var(--safe-r))
      calc(var(--rhythm) * 1.4 + var(--safe-b)) calc(var(--pad) + var(--safe-l));
  }

  /* the four links have nowhere to sit in a row this narrow */
  .hud__col--end .hud__row {
    flex-wrap: wrap;
    justify-content: flex-end;
    gap: 4px var(--gap);
  }

  .hud__row--split { gap: var(--gap); }

  .hero__name,
  .hero__role {
    position: relative;
    left: auto;
    right: auto;
    top: auto;
    bottom: auto;
  }

  .hero__role--a { margin-top: var(--gap); }
  .hero__role--b { margin-top: var(--gap-sm); }

  .work__cols { flex-direction: column; gap: var(--rhythm); }
  .work > * + .work__cols,
  .work__cols + .project,
  .work > .project + .project { margin-top: var(--rhythm); }

  /* a short screen (a phone on its side) cannot take the name and role at
     their width-based sizes: they are capped by the screen's height */
  .hero__name { font-size: min(var(--fs-name), 13svh); }   /* js/main.js fits it to the width */
  .hero__role { font-size: min(var(--fs-role), 7svh); }

  /* the role steps in off the name, to the frame's second axis — where
     the language item stands once the name has docked — by ink, as on a
     desktop (the frame's columns never move; only its top row's items
     slide while the page is at the top) */
  .hero__role { margin-left: calc(var(--hud-axis-2, 0px) - var(--pad) - var(--hero-optical, 0px)); }

  /* one stack, in the order of the cases: the columns give their cards
     up to the pair, which orders them (js/i18n.js sets each card's order) */
  .work__col { display: contents; }
  .work__cols > .work__col > .project { width: 100%; margin-top: 0; }

  .project--inset { width: 100%; margin-left: 0; }}

/* ---- the frame on a phone ----
   The foot row of the frame (city, clock) is not needed this narrow. The
   links go down there instead — CV, Telegram, the address, LinkedIn —
   spread across the whole width, and the top keeps the name and the
   language. The frame's type is a size larger, so it can be read and hit. */
@media (max-width: 600px) {
  .hud { font-size: 13px; line-height: 16px; }
  .case__rail { font-size: 13px; line-height: 16px; }

  .hud__foot { display: none; }

  .hud .hud__col--end > .hud__row:not(.hud__foot) {
    /* absolute against the frame's padding box, which is the whole
       screen — so it takes the safe area itself, as the padding does */
    position: absolute;
    left: calc(var(--pad) + var(--safe-l));
    right: calc(var(--pad) + var(--safe-r));
    bottom: calc(var(--pad) + var(--safe-b));
    flex-wrap: nowrap;
    justify-content: space-between;
    gap: 8px;
    /* docks under the About rule at the end of the page, as the foot row did */
    transform: translateY(calc(-1 * var(--dock, 0px)));
  }
}

/* ---- the type scale on a phone and a tablet ----
   The fluid scale is drawn for a 1440 screen, and below 1000px every step
   of it sits on its floor: labels at 10px, running copy at 13. This narrow,
   the small voice is the frame's own 13/16, running copy is 16/20, and the
   job descriptions read at the tables' 14/20. */
@media (max-width: 900px) {
  :root {
    --fs-mono: 13px;
    --lh-mono: 16px;
    --fs-body: 16px;
    --lh-copy: 20px;
  }
  /* what a job says is running copy, so on a phone it is set at the size
     the paragraphs under the carousel are — the block reads as one voice
     rather than as a table that shrank. The years, the company and the
     role stay the frame's own small condensed voice, as labels do
     everywhere on the site. */
  .about__job-desc { font-size: var(--fs-body); line-height: var(--lh-copy); }

  /* a card's caption takes the whole column here: the cards are stacked
     and full width this narrow, and --w-copy, drawn for a 460 measure on
     the canvas, bottoms out at 240 and leaves the line breaking early */
  .project__desc { width: 100%; }
}
