/* ============================================================
   site.css - the whole stylesheet for the portfolio.

   Lifted verbatim out of the <style> block that used to sit in
   index.html. Nothing here changed in the move: it is byte for
   byte the same sheet, in the same order, so the cascade is the
   cascade it always was.

   It lives in its own file now because three pages are going to
   share it, and a stylesheet duplicated across three documents
   is a stylesheet that drifts.
   ============================================================ */

:root{
  --paper:#f1e4d1; --paper-2:#e9d9c0; --ink:#1c1815; --ink-2:#5a5049; --ink-3:#655b52;
  /* Two weights, because on a light ground one alpha cannot do both jobs.
     --line is for a control whose border is its ONLY boundary cue, so it
     has to clear 3:1. --rule is a hairline divider - decorative, no
     contrast floor, and at .55 it would read as a table border. */
  --line:rgba(28,24,21,.55); --rule:rgba(28,24,21,.11); --card:rgba(28,24,21,.05);

  /* One accent, but two roles. On the old dark ground #c8452d cleared 4.5:1
     as both a fill and as text. On cream it does not: 3.91:1 over the
     deepest wash stop. So the fill keeps the exact brand colour and small
     text gets a darker sibling - same hue, legal everywhere. */
  --accent:#c8452d; --accent-ink:#a03722;

  /* One signal, not twelve. Every project used to own a colour pair,
     which read as decoration rather than information. */ 
  --rust:var(--accent);   --amber:var(--accent);
  --forest:var(--accent); --violet:var(--accent);
  --navy:var(--accent);   --mustard:var(--accent);
  --plum:var(--accent);   --clay:var(--accent);
  --teal:var(--accent);   --coral:var(--accent);
  --oxblood:var(--accent);--sage:var(--accent);
  --sans:'Inter',system-ui,-apple-system,sans-serif;
  --mono:'JetBrains Mono',ui-monospace,monospace;
  --serif:'Instrument Serif',Georgia,'Times New Roman',serif;
  --sans-ar:'IBM Plex Sans Arabic','Noto Kufi Arabic',var(--sans);
  --display-ar:'Noto Kufi Arabic','IBM Plex Sans Arabic',var(--sans);
  --pad:clamp(20px,5vw,64px); --maxw:1400px;
  /* two radii, not eight: controls and surfaces */
  --r:2px; --r-lg:4px;

  /* motion — three curves and one duration scale. Every transition in the
     sheet pulls from here, so the whole page moves with the same accent
     instead of nineteen hand-typed durations that almost agree. */
  --ease-out:cubic-bezier(.23,1,.32,1);      /* entering, exiting, pressing */
  --ease-in-out:cubic-bezier(.77,0,.175,1);  /* moving across the screen */
  --ease-drawer:cubic-bezier(.32,.72,0,1);   /* the menu opening */
  --t-press:160ms;   /* button feedback */
  --t-hover:200ms;   /* hover and colour */
  --t-move:260ms;    /* something travelling, or a panel closing */
  --t-panel:420ms;   /* the menu opening */
  --t-reveal:550ms;  /* scroll reveal, hero entrance */
}
*{margin:0;padding:0;box-sizing:border-box}
/* the gutter is reserved always, so locking the body for the menu can't
   take the scrollbar away and shift the whole page sideways */
html{scroll-behavior:smooth;background:#f1e4d1;scrollbar-gutter:stable}
/* smooth scrolling is the largest movement on the page — it goes too */
@media(prefers-reduced-motion:reduce){html{scroll-behavior:auto}}
body{background:transparent;color:var(--ink);font-family:var(--sans);-webkit-font-smoothing:antialiased;overflow-x:hidden}

/* ════════════════════════════════════════════════════
   SECTION COLOUR
   One fixed radial wash behind the whole page: light in
   the middle, deeper at the edges. Registering the two
   stops with @property is what lets them *animate* —
   plain custom properties would snap between sections.
   Every tone sits in one warm family around the accent.
   ════════════════════════════════════════════════════ */
@property --c0{syntax:'<color>';inherits:true;initial-value:#f1e8dc}
@property --c1{syntax:'<color>';inherits:true;initial-value:#efe6d8}
@property --c2{syntax:'<color>';inherits:true;initial-value:#f0e7da}
@property --c3{syntax:'<color>';inherits:true;initial-value:#efe5d7}
@property --c4{syntax:'<color>';inherits:true;initial-value:#f1e8db}

.bg{
  position:fixed;inset:0;z-index:-2;pointer-events:none;
  background:
    radial-gradient(ellipse 70% 60% at 14% 18%, var(--c1), transparent 62%),
    radial-gradient(ellipse 64% 56% at 88% 12%, var(--c2), transparent 64%),
    radial-gradient(ellipse 72% 62% at 24% 86%, var(--c3), transparent 62%),
    radial-gradient(ellipse 66% 58% at 84% 80%, var(--c4), transparent 64%),
    var(--c0);
  /* plain `ease` on purpose: a colour crossfade wants an even rate, and a
     strong curve would hold the old tone, rush, then hold the new one */
  transition:
    --c0 1.15s ease, --c1 1.15s ease, --c2 1.15s ease,
    --c3 1.15s ease, --c4 1.15s ease;
}
/* a whisper of grain so the flat fields don't look digital */
.bg::after{
  content:'';position:absolute;inset:0;opacity:.42;mix-blend-mode:multiply;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='.16'/%3E%3C/svg%3E");
}
@media(prefers-reduced-motion:reduce){.bg{transition:none}}

/* One warm family, not twelve hues. Sections still shift as you scroll,
   but in temperature and depth — never off into green, blue or pink. */
html[data-tone="ivory"] .bg{--c0:#f1e8db;--c1:#ede1d3;--c2:#ede1d4;--c3:#efe4d7;--c4:#f0e7da}
html[data-tone="ink"] .bg{--c0:#f1e8dc;--c1:#efe6d8;--c2:#f0e7da;--c3:#efe5d7;--c4:#f1e8db}
html[data-tone="stone"] .bg{--c0:#f1e8dc;--c1:#ede3d6;--c2:#efe4d8;--c3:#ede2d5;--c4:#f0e7db}
html[data-tone="blush"] .bg{--c0:#f1e7db;--c1:#ede1d3;--c2:#ede1d4;--c3:#eee4d7;--c4:#f0e6d9}
html[data-tone="moss"] .bg{--c0:#f1e9dc;--c1:#eee6d6;--c2:#efe7d7;--c3:#efe6d8;--c4:#f1e8db}
html[data-tone="slate"] .bg{--c0:#f0e8dc;--c1:#ebe4d9;--c2:#ece5da;--c3:#eee4d6;--c4:#efe7dc}
html[data-tone="mauve"] .bg{--c0:#f1e7db;--c1:#ede0d4;--c2:#eee2d6;--c3:#efe3d7;--c4:#f0e6da}
html[data-tone="lagoon"] .bg{--c0:#f1e8db;--c1:#ede4d6;--c2:#efe5d7;--c3:#eee4d5;--c4:#f0e7da}
html[data-tone="oat"] .bg{--c0:#f1e8db;--c1:#ede3d4;--c2:#eee4d6;--c3:#efe5d7;--c4:#f0e8da}
html[data-tone="ash"] .bg{--c0:#f1e8dd;--c1:#efe4d8;--c2:#efe5da;--c3:#eee4d7;--c4:#f1e7dc}
html[data-tone="bronze"] .bg{--c0:#f1e8db;--c1:#ede3d3;--c2:#eee4d5;--c3:#efe5d7;--c4:#f0e8da}
html[data-tone="close"] .bg{--c0:#f1e7db;--c1:#ede1d3;--c2:#ede1d4;--c3:#eee3d6;--c4:#f0e7da}


img,video{display:block;max-width:100%}
a{color:inherit}
.shell{max-width:var(--maxw);margin:0 auto;padding:0 var(--pad)}

/* ── reveal ────────────────────────────────────
   A short lift, carried on `translate` rather than `transform` so a card can
   be revealing and tilting at the same time without either clobbering the
   other. It used to take a full second and rotate in 3D, which meant blocks
   were still arriving as you started reading them.
   The delay is a variable rather than `transition-delay` so a staggered card
   doesn't also inherit the delay on its hover and tilt.  */
.rv{
  opacity:0;translate:0 18px;
  transition:opacity var(--t-reveal) var(--ease-out) var(--rv-d,0ms),
             translate var(--t-reveal) var(--ease-out) var(--rv-d,0ms);
}
.rv.in{opacity:1;translate:none}

/* Blocks that enter as a group cascade by their position in it. The old
   stagger counted position within the observer batch, so the same row
   animated differently depending on how fast you happened to be scrolling. */
.bento .b:nth-child(2),.arch .layer:nth-child(2){--rv-d:60ms}
.bento .b:nth-child(3),.arch .layer:nth-child(3){--rv-d:120ms}
.bento .b:nth-child(4),.arch .layer:nth-child(4){--rv-d:180ms}
.bento .b:nth-child(5),.arch .layer:nth-child(5){--rv-d:240ms}
.bento .b:nth-child(6),.arch .layer:nth-child(6){--rv-d:300ms}

@media(prefers-reduced-motion:reduce){
  .rv{opacity:1;translate:none;transition:none}
  .vis,.b,.step figure img{transition:none!important}
}

/* ── page transitions ───────────────────────────
   Cross-document, which is the whole reason this site is
   three real documents instead of one with a router: the
   header and footer are NAMED, so they are matched across
   pages and carried over rather than redrawn. Everything
   between them cross-fades.

   Firefox has no cross-document support yet and simply
   navigates normally - the header is position:fixed either
   way, so nothing here is load-bearing. */
@view-transition{navigation:auto}

#nav{view-transition-name:site-header}
.foot{view-transition-name:site-footer}

/* short out, slightly longer in: leaving should feel quicker
   than arriving, or the new page reads as sluggish */
::view-transition-old(root){animation:vtOut 150ms var(--ease-out) both}
::view-transition-new(root){animation:vtIn 300ms var(--ease-out) both}
@keyframes vtOut{to{opacity:0}}
@keyframes vtIn{from{opacity:0;translate:0 9px}}

@media(prefers-reduced-motion:reduce){
  /* Two layers on purpose. The pseudo-element rules mute the animation and
     are the reliable part. Declining the navigation outright is the real
     opt-out - it skips snapshot capture entirely rather than animating to
     nowhere - but support for @view-transition inside @media is newer, so
     it cannot be the only line of defence. */
  @view-transition{navigation:none}
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*){animation:none!important}
}

/* forced-colors and reduced-motion are independent settings: a High Contrast
   user who has not asked for less motion was getting the full cross-fade with
   nothing here moderating it. */
@media (forced-colors:active){
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*){animation:none!important}
}

/* ── header ─────────────────────────────────────
   Fixed, and present at every width. There is no hamburger at
   any breakpoint: the sections are the point of the site, so
   they stay on screen and the wordmark shrinks instead.
   The divider under the bar is permanent now - it used to
   appear only once you had scrolled, which meant the header
   floated unanchored at the top of every page. */
#nav{
  position:fixed;top:0;left:0;right:0;z-index:100;
  -webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);
  background:rgba(241,228,209,.88);
  border-bottom:1px solid var(--rule);
  transition:border-color var(--t-move) ease,background-color var(--t-move) ease;
}

/* ── scroll progress ───────────────────────────
   A hairline along the bottom edge of the bar reporting how far down a very
   long page you are. Scroll-driven, so it runs off the main thread and needs
   no JavaScript at all; browsers without it simply don't get the line.
   It stays under reduced motion — it only ever moves because the reader
   moved, which is the thing that setting is not about. */
@supports (animation-timeline:scroll()){
  #nav::after{
    content:'';position:absolute;inset-inline:0;bottom:-1px;height:2px;
    background:var(--accent);transform:scaleX(0);transform-origin:0 50%;
    animation:scrolled linear both;animation-timeline:scroll(root block);
  }
  [dir="rtl"] #nav::after{transform-origin:100% 50%}
  @keyframes scrolled{to{transform:scaleX(1)}}
}

.nav-in{
  max-width:var(--maxw);margin:0 auto;
  padding:13px var(--pad);
  display:flex;align-items:center;gap:clamp(10px,2vw,26px);
}

/* wordmark doubles as the home link. The full name stays in the DOM at
   every width - swapping it for initials would leave voice-control users
   saying a name that is no longer on screen. It just gets smaller. */
.mark{
  text-decoration:none;color:var(--ink);font-family:var(--mono);
  font-size:clamp(.62rem,1.6vw,.78rem);letter-spacing:.15em;
  text-transform:uppercase;font-weight:500;white-space:nowrap;
  transition:color var(--t-hover) ease;
}
.mark:hover{color:var(--accent-ink)}

.avail{display:flex;align-items:center;gap:8px;font-family:var(--mono);font-size:.7rem;color:var(--ink-3);letter-spacing:.02em;white-space:nowrap}
.pulse{width:6px;height:6px;border-radius:50%;background:var(--accent);flex:none}

.nav-mid{margin-inline-start:auto;display:flex;align-items:center;gap:clamp(12px,1.9vw,26px);font-size:.86rem;font-weight:500}
.nav-mid a{
  position:relative;text-decoration:none;color:var(--ink-2);
  padding:5px 0;white-space:nowrap;transition:color var(--t-hover) ease;
}
.nav-mid a:hover{color:var(--ink)}
/* current page is marked with a rule in the accent, not a pill or a chip */
.nav-mid a[aria-current="page"]{color:var(--ink)}
.nav-mid a[aria-current="page"]::after{
  content:'';position:absolute;inset-inline:0;bottom:-1px;height:2px;background:var(--accent);
}

/* ── language ───────────────────────────────────
   A globe and the code for the language you are in. The popover
   is NOT a modal: it does not trap focus and it does not lock the
   page, because picking a language is a one-tap errand and being
   held inside a dialog for it is worse than the problem it solves.
   The three buttons keep their data-lang, so i18n.js still owns
   them and still sets aria-current on the active one. */
/* <details> needs the marker suppressed and the summary styled as the button
   it replaces. list-style on the summary AND the ::-webkit-details-marker
   pseudo: Safari ignores the first. */
.lang-pick{position:relative;flex:none}
.lang-pick > summary{list-style:none;cursor:pointer}
.lang-pick > summary::-webkit-details-marker{display:none}
.lang-pick > summary::marker{content:''}
.lang-pick[open] > summary{border-color:var(--ink);background:var(--paper-2)}
.globe{
  display:flex;align-items:center;gap:7px;cursor:pointer;
  border:1px solid var(--line);background:var(--card);color:var(--ink);
  border-radius:var(--r);padding:6px 9px;
  font-family:var(--mono);font-size:.68rem;letter-spacing:.1em;line-height:1;
  transition:border-color var(--t-hover) ease,background-color var(--t-hover) ease;
}
.globe:hover{border-color:var(--ink)}

.globe svg{width:15px;height:15px;flex:none;fill:none;stroke:currentColor;stroke-width:1.3}

.lang-pop{
  position:absolute;top:calc(100% + 10px);inset-inline-end:0;
  min-width:180px;padding:5px;z-index:10;
  background:var(--paper);border:1px solid var(--rule);border-radius:var(--r-lg);
  box-shadow:0 22px 48px -28px rgba(28,24,21,.4);
  display:flex;flex-direction:column;
}

.lang-pop :is(a,button){
  text-decoration:none;
  display:flex;align-items:center;justify-content:space-between;gap:14px;
  font-family:inherit;font-size:.9rem;text-align:start;cursor:pointer;
  background:none;border:0;color:var(--ink-2);padding:9px 11px;border-radius:var(--r);
  transition:background-color var(--t-hover) ease,color var(--t-hover) ease;
}
.lang-pop :is(a,button):hover{background:var(--paper-2);color:var(--ink)}
.lang-pop :is(a,button)[aria-current="true"]{color:var(--ink);font-weight:600}
.lang-pop :is(a,button)[aria-current="true"]::after{
  content:'';width:5px;height:5px;border-radius:50%;background:var(--accent);flex:none;
}
.lang-pop :is(a,button)[lang="ar"]{font-family:var(--sans-ar)}

@media(prefers-reduced-motion:reduce){.lang-pop{transition:none}}

/* the availability line is the first thing to go when the bar tightens;
   the navigation itself never collapses */
@media(max-width:760px){.avail{display:none}}
@media(max-width:520px){
  .nav-in{padding:11px var(--pad);gap:8px}
  .nav-mid{gap:13px;font-size:.8rem}
  .globe{padding:6px 7px;gap:5px}
}
/* Below this the wordmark, three links and the globe stop fitting on one
   line, and the globe - the last item - was being clipped off the edge,
   putting the language switcher out of reach on a small phone. Wrap to two
   rows instead of shrinking further: the wordmark stays a full, readable
   home link and every control keeps a 44px target. */
@media(max-width:390px){
  .nav-in{flex-wrap:wrap;row-gap:8px}
  .mark{font-size:.66rem}
  .lang-pick{margin-inline-start:auto}
  .nav-mid{
    order:3;flex-basis:100%;margin-inline-start:0;
    justify-content:space-between;font-size:.82rem;
  }
}

/* ── skip link ──────────────────────────────────
   There was no way to jump the header before this. It is the
   first focusable thing in the document and it stays off screen
   until it is focused. */
.skip{
  position:fixed;top:9px;inset-inline-start:9px;z-index:200;
  background:var(--ink);color:var(--paper);text-decoration:none;
  padding:10px 16px;border-radius:var(--r);font-size:.85rem;font-weight:600;
  transform:translateY(-180%);
  transition:transform var(--t-move) var(--ease-out);
}
.skip:focus{transform:none}
@media(prefers-reduced-motion:reduce){.skip{transition:none}}

/* the skip target is a landmark, not a control - it takes focus so the
   jump is announced, but it should not then draw a ring around the page */
#main:focus{outline:none}

/* ── forced colours ─────────────────────────────
   Windows High Contrast throws away background-color on
   decorative pseudo-elements, which is exactly what both
   of this header's state indicators were made of: the
   scroll bar and the current-page underline. Repaint them
   in system colours and opt them out of the override.
   Nothing else in this sheet handled forced-colors at all. */
@media (forced-colors:active){
  #nav{border-bottom-color:CanvasText}
  #nav::after,
  .nav-mid a[aria-current="page"]::after,
  .lang-pop :is(a,button)[aria-current="true"]::after{
    background:Highlight;forced-color-adjust:none;
  }
  /* current page also loses its ink/ink-2 distinction, so give it a second cue */
  .nav-mid a[aria-current="page"]{text-decoration:underline;text-underline-offset:5px}
  .lang-pop{border-color:CanvasText}
  .globe[aria-expanded="true"]{border-color:Highlight}
  .skip{border:1px solid CanvasText}
}

/* ── hero ──────────────────────────────────────── */
/* 100svh meant the hero ate the entire screen on every device, so the page
   opened on a wall with nothing hinting there was more. 82svh on a desktop
   and 76 on a phone keeps it dominant while letting the next section show
   its top edge. */
.hero{min-height:82svh;display:flex;flex-direction:column;justify-content:flex-end;padding-bottom:clamp(20px,3.4vh,42px);position:relative}
@media(max-width:760px){.hero{min-height:76svh}}
/* A phone was rendering everything a size too large - the whole page read as
   zoomed. One step down on the type that drives the layout. */
@media(max-width:640px){
  html{font-size:94%}
  .hero-name{font-size:clamp(1.9rem,9.4vw,3.3rem)}
  .hero-role{font-size:1rem}
  .head h1,.head h2{font-size:clamp(1.7rem,8vw,2.3rem)}
  .proj h2{font-size:clamp(1.6rem,7.6vw,2.2rem)}
  .lede,.proj .lede{font-size:.97rem}
}

/* photo: wider frame, cropped at the feet, edges melting into the paper */
.hero-portrait{
  position:absolute;left:50%;transform:translateX(-50%);bottom:0;z-index:2;
  width:min(41vw,500px);height:min(72svh,680px);
}
/* the whole photo is the control - no arrows, no dots */
.hero-shot{
  display:block;width:100%;height:100%;position:relative;
  padding:0;border:0;background:none;cursor:pointer;
}
.hero-shot img{position:absolute;inset:0;opacity:0;transition:opacity 620ms var(--ease-out)}
.hero-shot img.on{opacity:1}
/* the ring has to come inside: the mask fades the edges to nothing, so an
   outset outline would draw in mid-air away from anything visible */
.hero-shot:focus-visible{outline-offset:-6px}

.hero-count{
  position:absolute;bottom:8px;inset-inline-end:6px;z-index:3;
  /* "1 / 2" is a numeric expression, not a sentence: it must not mirror.
     Left to the flex row it reversed under RTL and read as "2 / 1". */
  direction:ltr;
  font-family:var(--mono);font-size:.66rem;letter-spacing:.14em;color:var(--ink-3);
  display:flex;gap:5px;pointer-events:none;
}
.hero-count-sep{opacity:.45}
/* one photo is not a sequence */
.hero-portrait:has(img:only-of-type) .hero-count{display:none}

.hero-portrait img{
  width:100%;height:100%;object-fit:cover;object-position:50% 12%;  /* per-photo override lives inline */
  filter:grayscale(1) contrast(1.07) brightness(1.03);
  /* Fades on all four edges, but the TOP one is deliberately short.
     The portrait is square and the frame is taller than it is wide, so
     `cover` shows the photograph's full height - which puts his head hard
     against the top edge. A 14% fade there, sized to match the sides, ate
     the forehead and hair. 5% still softens the ruled line the dark studio
     backdrop makes against paper, without cropping the face. */
  -webkit-mask-image:
    linear-gradient(90deg,transparent,#000 16%,#000 84%,transparent),
    linear-gradient(180deg,transparent,#000 5%,#000 78%,transparent);
  mask-image:
    linear-gradient(90deg,transparent,#000 16%,#000 84%,transparent),
    linear-gradient(180deg,transparent,#000 5%,#000 78%,transparent);
  -webkit-mask-composite:source-in; mask-composite:intersect;
}

/* name: staggered two-line lockup riding over the photo.
   paper-coloured halo keeps every letter readable where it crosses the image. */
.hero-name{
  /* Was 9.9vw / 9.5rem, which at any real width ran clean across the
     portrait and buried the face. Smaller lets the photograph read. */
  font-size:clamp(2.1rem,7.2vw,6.6rem);font-weight:900;line-height:.9;
  text-transform:uppercase;position:relative;z-index:3;
  font-optical-sizing:auto;font-feature-settings:"kern" 1,"liga" 1;
  text-shadow:
    0 0 40px rgba(241,228,209,.95),
    0 0 16px rgba(241,228,209,.9),
    0 2px 26px rgba(241,228,209,.62);
}
.hero-name .l1{display:block;letter-spacing:-.052em}
.hero-name .l2{display:block;margin-inline-start:26%;letter-spacing:-.058em;position:relative}
/* hairline running out of the indent, stopping just before line two */
.hero-name .l2::before{
  content:'';position:absolute;inset-inline-end:calc(100% + 26px);top:50%;
  width:min(17vw,210px);height:1px;background:var(--ink);opacity:.26;
}
.hero-idx{
  font-family:var(--mono);font-size:.7rem;letter-spacing:.22em;color:var(--ink-3);
  text-transform:uppercase;margin-bottom:clamp(10px,1.6vh,18px);position:relative;z-index:3;
  display:flex;align-items:center;gap:12px;
}
.hero-idx::after{content:'';flex:1;max-width:90px;height:1px;background:var(--rule)}
@media(max-width:640px){
  .hero-name .l2{margin-inline-start:14%}
  .hero-name .l2::before{display:none}
}
.hero-meta{display:flex;justify-content:space-between;align-items:flex-end;gap:30px;position:relative;z-index:3;margin-top:12px;flex-wrap:wrap}
.hero-role{font-size:clamp(1rem,2.2vw,1.35rem);font-weight:600;max-width:22ch;line-height:1.3}
.hero-role>span{display:block}
.hero-role .sub{color:var(--ink-3);font-weight:400;font-size:.86rem;margin-top:8px;font-family:var(--mono)}
.hero-links{display:flex;gap:10px}
.hero-links a{
  width:42px;height:42px;border-radius:var(--r);border:1px solid var(--line);background:var(--card);
  display:grid;place-items:center;color:var(--ink-2);
  transition:transform var(--t-press) var(--ease-out),border-color var(--t-hover) ease,
             box-shadow var(--t-hover) ease,background-color var(--t-hover) ease;
}
.hero-links svg{width:18px;height:18px;fill:currentColor}

@media(max-width:700px){.hero-portrait{width:56vw}}

/* ── hero entrance ─────────────────────────────
   The rest of the page lifts into view as you scroll, and the one moment
   that actually needs choreography — the first three seconds — had none.
   A short cascade: place, name, role, links. CSS animation rather than JS
   because this fires while fonts and a large portrait are still loading,
   and CSS keeps running off the main thread while that happens. */
@keyframes heroIn{from{opacity:0;translate:0 14px}to{opacity:1;translate:none}}
@keyframes heroPortraitIn{from{opacity:0;scale:1.03}to{opacity:1;scale:1}}

.hero-idx,.hero-name .l1,.hero-name .l2,.hero-role,.hero-links{
  opacity:0;animation:heroIn var(--t-reveal) var(--ease-out) forwards;
}
.hero-idx{animation-delay:60ms}
.hero-name .l1{animation-delay:120ms}
.hero-name .l2{animation-delay:180ms}
.hero-role{animation-delay:260ms}
.hero-links{animation-delay:320ms}
/* the portrait carries translateX(-50%) for centring, so it arrives on
   `scale` and leaves `transform` alone */
.hero-portrait{opacity:0;animation:heroPortraitIn 700ms var(--ease-out) forwards}

/* Once per visit, not once per page. The staggered entrance is an arrival
   flourish; with cross-document transitions it was replaying on every hop
   between the three pages, layering a second fade on top of the page fade.
   `settled` is set in the head script from a session flag, before paint,
   so the animation never starts rather than being cut off mid-way. */
html.settled .hero-idx,
html.settled .hero-name .l1,
html.settled .hero-name .l2,
html.settled .hero-role,
html.settled .hero-links,
html.settled .hero-portrait{animation:none;opacity:1}

@media(prefers-reduced-motion:reduce){
  .hero-shot img{transition:none}
  /* gentler, not gone: everything still fades up together, nothing travels */
  .hero-idx,.hero-name .l1,.hero-name .l2,.hero-role,.hero-links,.hero-portrait{
    animation:heroFade var(--t-move) ease both;
  }
  @keyframes heroFade{from{opacity:0}to{opacity:1}}
}

/* ── capability ticker ─────────────────────────
   The stack every project below is built on, moving slowly enough to read
   and pausing the moment you point at it. The list is duplicated in the
   markup so the -50% loop is seamless, and the mask fades both edges rather
   than clipping them. `linear` is right here — constant motion never eases,
   and 48s per lap is deliberately slow; a fast ticker reads as decoration. */
.ticker{
  overflow:hidden;border-block:1px solid var(--rule);padding-block:14px;
  margin-top:clamp(30px,5vh,60px);
  -webkit-mask-image:linear-gradient(90deg,transparent,#000 7%,#000 93%,transparent);
  mask-image:linear-gradient(90deg,transparent,#000 7%,#000 93%,transparent);
}
.ticker-track{display:flex;width:max-content;animation:ticker 48s linear infinite}
.ticker:hover .ticker-track,.ticker:focus-within .ticker-track{animation-play-state:paused}
/* Not animation-direction:reverse. In RTL the flex row already lays the two
   sets out right-to-left, so the track begins at the right edge; reversing a
   keyframe that ends at -50% starts it AT -50%, with the visible strip empty
   until the cycle brings content back. Its own keyframe travels the other way
   from 0, so the bar is full from the first frame like the other languages. */
[dir="rtl"] .ticker-track{animation-name:tickerRtl}
@keyframes tickerRtl{to{transform:translateX(50%)}}
@keyframes ticker{to{transform:translateX(-50%)}}
.ticker-set{
  display:flex;align-items:center;list-style:none;
  gap:clamp(22px,3.4vw,46px);padding-inline-end:clamp(22px,3.4vw,46px);
}
.ticker-set li{
  display:flex;align-items:center;gap:clamp(22px,3.4vw,46px);
  font-family:var(--mono);font-size:.78rem;letter-spacing:.04em;
  color:var(--ink-2);white-space:nowrap;
}
.ticker-set li::before{
  content:'';width:4px;height:4px;border-radius:50%;
  background:var(--accent);opacity:.55;flex:none;
}
@media(prefers-reduced-motion:reduce){
  /* nothing travels: the strip becomes a plain wrapped list of the same facts */
  .ticker{-webkit-mask-image:none;mask-image:none}
  .ticker-track{animation:none;width:100%}
  .ticker-set{flex-wrap:wrap;row-gap:10px}
  .ticker-set:last-child{display:none}
}

/* ── bento (dark break) ────────────────────────── */
.bento-wrap{background:transparent;color:var(--ink);padding:clamp(54px,9vh,110px) 0;margin-top:clamp(40px,7vh,90px)}
.bento-head{display:flex;align-items:baseline;justify-content:space-between;gap:20px;margin-bottom:clamp(20px,3vh,34px);flex-wrap:wrap}
.bento-head .eyebrow{color:var(--accent-ink);margin:0}
.bento-head .who{font-family:var(--serif);font-size:clamp(1.4rem,3vw,2.1rem);font-style:italic;color:var(--ink-3)}

.bento{
  display:grid;grid-template-columns:repeat(12,1fr);gap:12px;
  grid-auto-rows:minmax(96px,auto);perspective:1400px;
}
/* the reveal properties are repeated here because this rule would otherwise
   override `.rv`'s transition wholesale — which it did, and the cards were
   snapping their opacity instead of fading. `will-change` is gone: it was
   promoting six layers permanently, including on touch where the tilt that
   justified it never runs. The tilt adds it on pointer enter instead. */
.b{
  border-radius:var(--r-lg);padding:clamp(20px,2.6vw,32px);position:relative;overflow:hidden;
  transition:opacity var(--t-reveal) var(--ease-out) var(--rv-d,0ms),
             translate var(--t-reveal) var(--ease-out) var(--rv-d,0ms),
             transform var(--t-move) var(--ease-out),
             box-shadow var(--t-hover) ease,border-color var(--t-hover) ease;
  transform-style:preserve-3d;
}
.b.cream{background:#e3d9c4;color:#171610}
.b.dark{background:#1e1c16;border:1px solid #2e2b22;color:#efe9dc}
.b.dark:hover{border-color:#c8a86b}

.b-say{grid-column:span 5;grid-row:span 3;display:flex;flex-direction:column;justify-content:space-between}
.b-say h2{font-family:var(--serif);font-weight:400;font-size:clamp(1.75rem,3.5vw,2.9rem);line-height:1.12;letter-spacing:-.015em}
.b-say h2 em{font-style:italic;color:#7a6a3f}
.b-say .sig{font-family:var(--mono);font-size:.7rem;letter-spacing:.16em;text-transform:uppercase;color:#6d6552;margin-top:24px}

.b-face{grid-column:span 3;grid-row:span 5;padding:0;background:#0f0e0a}
.b-face img{width:100%;height:100%;object-fit:cover;object-position:50% 22%;filter:contrast(1.05) saturate(1.04)}
.b-face figcaption{
  position:absolute;left:16px;right:16px;bottom:16px;font-family:var(--mono);font-size:.68rem;
  letter-spacing:.12em;text-transform:uppercase;color:#efe9dc;text-shadow:0 2px 12px rgba(0,0,0,.9);
}

.b-list{grid-column:span 4;grid-row:span 3}
.b-list h3{font-family:var(--mono);font-size:.7rem;letter-spacing:.16em;text-transform:uppercase;color:#8d8676;margin-bottom:14px}
.b-list a{
  display:flex;align-items:center;gap:12px;text-decoration:none;color:#efe9dc;
  padding:13px 0;border-bottom:1px solid #2e2b22;font-size:.98rem;
  /* the nudge on hover used to animate padding — layout work on every frame */
  transition:color var(--t-hover) ease,translate var(--t-hover) var(--ease-out);
}
.b-list a:last-child{border-bottom:0}
.b-list a span:last-child{
  margin-inline-start:auto;font-family:var(--mono);font-size:.72rem;color:#6d6552;
  transition:color var(--t-hover) ease;text-transform:uppercase;letter-spacing:.08em;white-space:nowrap;
}
.b-list a:hover{color:#c8a86b}
.b-list a:hover span:last-child{color:#c8a86b}

.b-bio{grid-column:span 5;grid-row:span 2}
.b-bio p{font-size:.94rem;line-height:1.7;color:#171610;opacity:.85}
.b-bio p+p{margin-top:10px}

.b-lang{grid-column:span 2;grid-row:span 2;display:flex;flex-direction:column;justify-content:center;gap:9px}
.b-lang b{display:block;font-size:1.02rem;font-weight:600}
.b-lang small{font-family:var(--mono);font-size:.66rem;color:#8d8676;letter-spacing:.1em;text-transform:uppercase}

.b-cta{
  grid-column:span 2;grid-row:span 2;background:#c8a86b;color:#15140f;
  display:flex;flex-direction:column;justify-content:space-between;text-decoration:none;
}
.b-cta .t{font-family:var(--serif);font-style:italic;font-size:clamp(1.3rem,2.4vw,1.9rem);line-height:1.1}
.b-cta .arw{align-self:flex-end;font-size:1.5rem;transition:transform var(--t-move) var(--ease-out)}

@media(max-width:1000px){
  .b-say{grid-column:span 7;grid-row:span 3}
  .b-face{grid-column:span 5;grid-row:span 5}
  .b-list{grid-column:span 7;grid-row:span 3}
  .b-bio{grid-column:span 7;grid-row:span 2}
  .b-lang{grid-column:span 5;grid-row:span 2}
  .b-cta{grid-column:span 12;grid-row:span 1}
}
@media(max-width:640px){
  .bento{grid-template-columns:1fr}
  .b-say,.b-face,.b-list,.b-bio,.b-lang,.b-cta{grid-column:1/-1;grid-row:auto}
  .b-face{min-height:380px}
}

/* ── stats ─────────────────────────────────────── */
.stats{display:grid;grid-template-columns:repeat(4,1fr);gap:20px;padding:clamp(40px,7vh,80px) 0;border-top:1px solid var(--rule);border-bottom:1px solid var(--rule)}
@media(max-width:700px){.stats{grid-template-columns:repeat(2,1fr);gap:32px}}
.stat b{display:block;font-size:clamp(2.4rem,6vw,4.4rem);font-weight:800;letter-spacing:-.05em;line-height:1;color:var(--sc)}
.stat small{display:block;font-family:var(--mono);font-size:.7rem;color:var(--ink-3);margin-top:8px;letter-spacing:.06em;text-transform:uppercase}
.stat:nth-child(1){--sc:var(--ink)}
.stat:nth-child(2){--sc:var(--ink)}
.stat:nth-child(3){--sc:var(--ink)}
.stat:nth-child(4){--sc:var(--ink)}

/* ── section heads ─────────────────────────────── */
.head{padding:clamp(70px,11vh,140px) 0 clamp(28px,4vh,56px)}
.eyebrow{font-family:var(--mono);font-size:.72rem;color:var(--accent-ink);letter-spacing:.14em;text-transform:uppercase;margin-bottom:14px}
.head h1,.head h2{font-size:clamp(2rem,6.5vw,4.6rem);font-weight:900;letter-spacing:-.045em;line-height:.95;text-transform:uppercase}
.head p{color:var(--ink-2);margin-top:16px;max-width:56ch}

/* ── architecture ──────────────────────────────── */
.arch{display:flex;flex-direction:column;gap:10px;padding-bottom:clamp(50px,8vh,100px);perspective:1200px}
.layer{
  border:1px solid var(--rule);background:var(--card);border-radius:var(--r-lg);
  padding:clamp(16px,2.4vw,26px) clamp(18px,3vw,34px);
  display:flex;align-items:center;gap:20px;position:relative;overflow:hidden;
  /* reveal properties repeated for the same reason as .b — this rule would
     otherwise replace the one on .rv and the rows would snap into view */
  transition:opacity var(--t-reveal) var(--ease-out) var(--rv-d,0ms),
             translate var(--t-reveal) var(--ease-out) var(--rv-d,0ms),
             transform var(--t-move) var(--ease-out),
             border-color var(--t-hover) ease,box-shadow var(--t-move) ease;
  transform-style:preserve-3d;
}
.layer::before{content:'';position:absolute;inset-inline-start:0;top:0;bottom:0;width:3px;background:var(--lc);transform:scaleY(0);transition:transform var(--t-move) var(--ease-out);transform-origin:top}
.layer .n{font-family:var(--mono);font-size:.72rem;color:var(--lc);min-width:28px;font-weight:500}
/* the spine runs through the palette, top to bottom */
.layer:nth-child(1){--lc:var(--rust)}
.layer:nth-child(2){--lc:var(--violet)}
.layer:nth-child(3){--lc:var(--teal)}
.layer:nth-child(4){--lc:var(--mustard)}
.layer:nth-child(5){--lc:var(--navy)}
.layer:nth-child(6){--lc:var(--forest)}
.layer .t{font-weight:700;font-size:clamp(.95rem,1.9vw,1.2rem);letter-spacing:-.01em}
.layer .d{margin-inline-start:auto;font-family:var(--mono);font-size:.74rem;color:var(--ink-3);text-align:end}
@media(max-width:640px){.layer{flex-wrap:wrap;gap:8px}.layer .d{margin-inline-start:0;width:100%;text-align:start}}

/* ── projects ──────────────────────────────────── */
/* Each project is a case study, not a card. The old layout put the text
   in one column and the pictures in the other, both vertically centred -
   which caps every project at the height of its shorter side and makes
   seven substantial pieces of work read as seven tiles.

   Stacked instead: a header that gets room to breathe, the screenshot at
   full width where it can actually be looked at, the spec as a full-width
   band, and the detail last. Same words, same pictures, four times the
   presence. */
.proj{padding:clamp(80px,15vh,210px) 0;position:relative;perspective:1600px}
.proj+.proj{border-top:1px solid var(--rule)}

.proj-head{max-width:62ch;margin-bottom:clamp(34px,5.5vh,64px)}

/* the numeral is a chapter marker now, not a watermark: it sits on the
   line above the tag rather than ghosting behind the title */
.idx{
  font-family:var(--mono);font-size:.72rem;font-weight:500;letter-spacing:.22em;
  color:var(--accent-ink);opacity:1;margin-bottom:18px;
  display:flex;align-items:center;gap:12px;
}
.idx::after{content:'';flex:1;height:1px;background:var(--rule);max-width:120px}

.proj-foot{
  display:grid;grid-template-columns:1.1fr .9fr;
  gap:clamp(30px,5vw,70px);align-items:start;
  margin-top:clamp(30px,5vh,56px);
}
.proj.flip .proj-foot{grid-template-columns:.9fr 1.1fr}
.proj.flip .proj-say{order:2}
/* a project with no secondary shots gives its whole width to the writing */
.proj-foot:not(:has(.proj-more)){grid-template-columns:minmax(0,62ch)}
@media(max-width:940px){
  .proj-foot,.proj.flip .proj-foot{grid-template-columns:1fr}
  .proj.flip .proj-say{order:0}
}
.tag{display:inline-flex;align-items:center;gap:9px;font-family:var(--mono);font-size:.68rem;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-2);margin-bottom:14px}
.proj h2{
  font-size:clamp(2.2rem,6.4vw,4.4rem);font-weight:900;line-height:.9;letter-spacing:-.045em;
  text-transform:uppercase;margin-bottom:20px;
}
.proj h2 b{font-weight:900;color:var(--a1)}
.proj .lede{color:var(--ink-2);font-size:clamp(1.04rem,1.7vw,1.22rem);line-height:1.55;max-width:56ch}

/* spec strip — mono labels, hairline rules */
.spec{display:grid;grid-template-columns:repeat(3,1fr);gap:1px;background:var(--rule);border:1px solid var(--rule);margin:clamp(28px,4.5vh,52px) 0 0}
.spec div{background:var(--paper);padding:clamp(16px,2.2vw,24px) clamp(15px,2vw,22px)}
.spec dt{font-family:var(--mono);font-size:.62rem;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);margin-bottom:5px}
.spec dd{font-size:clamp(.9rem,1.3vw,1.02rem);font-weight:600;line-height:1.35}
@media(max-width:560px){.spec{grid-template-columns:1fr 1fr}}

.note-h{
  border-inline-start:2px solid var(--a1);padding-inline-start:16px;margin:22px 0 0;
  color:var(--ink-2);font-size:.95rem;max-width:48ch;
}
.note-h b{color:var(--ink);font-weight:700}

.chips{display:flex;flex-wrap:wrap;gap:6px;margin-top:22px}
.chip{
  font-family:var(--mono);font-size:.7rem;border:1px solid var(--rule);background:var(--card);
  padding:5px 11px;border-radius:var(--r);color:var(--ink-2);
  transition:border-color var(--t-hover) ease;
}
.proj:hover .chip{border-color:color-mix(in srgb,var(--a1) 28%,var(--line))}

.acts{display:flex;gap:10px;margin-top:28px;flex-wrap:wrap}
.act{
  text-decoration:none;font-size:.82rem;font-weight:600;padding:12px 22px;border-radius:var(--r);
  border:1px solid var(--ink);display:inline-flex;gap:8px;align-items:center;
  transition:background-color var(--t-hover) ease,color var(--t-hover) ease,
             border-color var(--t-hover) ease,transform var(--t-press) var(--ease-out);
}
.act:hover{background:var(--ink);color:var(--paper)}
.act.solid{background:var(--a1);border-color:var(--a1);color:var(--paper);font-weight:700}
.act.solid:hover{background:var(--ink);border-color:var(--ink);color:var(--paper)}

/* visual: flat offset block behind, like an off-register print run */
.vis{position:relative;transform-style:preserve-3d;transition:transform var(--t-panel) var(--ease-out)}

.shot{
  position:relative;overflow:hidden;border-radius:var(--r-lg);border:1px solid var(--rule);
  background:var(--card);transform:translateZ(0);
}
.shot img,.shot video{width:100%;height:auto;display:block;box-shadow:0 30px 70px -38px rgba(28,24,21,.30);transition:transform var(--t-move) var(--ease-out)}
/* parallax images are driven by scroll, so no transition to fight with */
.shot img[data-par]{transition:none;will-change:transform}

/* align-items:start stops a short cell being stretched to the tallest row,
   which is what left a clip floating in a box far taller than itself. */
.duo{display:grid;grid-template-columns:1fr 1fr;gap:12px;position:relative;align-items:start}
.proj-more{display:flex;flex-direction:column;gap:12px}
/* object-fit:cover on the video as well: if anything ever does constrain the
   box, the clip fills it rather than letterboxing inside it. */
.duo img,.duo video{width:100%;height:auto;object-fit:cover;border:1px solid var(--rule);border-radius:var(--r-lg);display:block}
.duo.one{grid-template-columns:1fr}

/* ── not found ──────────────────────────────────
   Short page, so it needs a floor - without one the footer
   rides up under the heading on a tall screen. */
.nf{min-height:62svh;display:flex;flex-direction:column;justify-content:center}
.nf .head{padding-top:0;max-width:54ch}
.nf .acts{margin-top:30px}

/* ── journey ───────────────────────────────────── */
/* The copy is capped at ~50ch for readability, so a full-width text column
   left a few hundred px of nothing between the words and the photo. Capping
   the timeline instead pulls the two together and hands the width to the
   images, which were the thing starved of it. Left-aligned, not centred, so
   it stays on the same axis as the section heading. */
.tl{display:grid;gap:0;padding-bottom:60px;max-width:1040px}
.step{display:grid;grid-template-columns:minmax(0,1fr) minmax(260px,430px);gap:clamp(22px,2.6vw,44px);align-items:start;padding:clamp(30px,4.6vh,52px) 0;border-top:1px solid var(--rule)}
@media(max-width:860px){.step{grid-template-columns:1fr;gap:16px}}
.step{perspective:1000px}
.step figure{transition:transform var(--t-move) var(--ease-out);transform-style:preserve-3d}
.step h3{font-size:clamp(1.3rem,2.9vw,2rem);font-weight:700;letter-spacing:-.02em;margin-bottom:12px}
.step p{color:var(--ink-2);font-size:clamp(1rem,1.15vw,1.08rem);line-height:1.7;max-width:50ch}
.step figure{overflow:hidden;border-radius:var(--r-lg);border:1px solid var(--rule)}
.step figure img,.step figure video{
  /* height:auto is what turns width/height attributes into an aspect ratio
     rather than a literal size. Without it these clips took height="1280"
     as a real 1280px box - a huge empty rectangle, since preload="none"
     means nothing is drawn in it until the poster or playback arrives. */
  width:100%;height:auto;object-fit:cover;filter:grayscale(1) contrast(1.05);
  transition:filter var(--t-reveal) var(--ease-out),transform var(--t-move) var(--ease-out);
}
/* touch devices have no hover, so scrolling into view does the reveal instead */
.step.lit figure img,.step.lit figure video{filter:grayscale(0) contrast(1.02)}
/* some steps carry several images, stacked in the media column */
.step .shots{display:grid;gap:16px}

/* ── writing ───────────────────────────────────── */
.post{max-width:66ch;padding:clamp(26px,4vh,44px) 0;border-top:1px solid var(--rule)}
.post h2{font-size:clamp(1.25rem,3vw,2rem);font-weight:700;letter-spacing:-.02em;line-height:1.22;margin-bottom:12px}
.post-lede{color:var(--ink-2);margin-bottom:18px}
.post-img{margin:0 0 20px;border:1px solid var(--rule);border-radius:var(--r-lg);overflow:hidden;background:var(--card)}
/* height:auto is required: the width/height attributes on the img reserve
   space against layout shift, but without this the browser uses that literal
   height and stretches the picture. */
.post-img img{width:100%;height:auto;display:block}
.post-body{border-top:1px solid var(--rule);padding-top:14px}
.post-body summary{
  cursor:pointer;list-style:none;font-family:var(--mono);font-size:.74rem;
  letter-spacing:.12em;text-transform:uppercase;color:var(--accent-ink);padding:4px 0;transition:opacity var(--t-hover) ease;
}
.post-body summary::-webkit-details-marker{display:none}
.post-body summary::after{content:' +'}
.post-body[open] summary::after{content:' –'}
.post-body summary:hover{opacity:.7}
.post-body .prose{padding-top:18px}
.post-body .prose p{color:var(--ink-2);line-height:1.8;margin-bottom:1.05em}
.post-body .prose p:last-child{margin-bottom:0}

/* ── closing ───────────────────────────────────── */
.end{padding:clamp(90px,16vh,190px) 0 clamp(50px,8vh,90px);border-top:1px solid var(--rule);text-align:center}
.end h2{font-size:clamp(2.1rem,9.5vw,7.5rem);font-weight:900;line-height:.9;letter-spacing:-.05em;text-transform:uppercase}
.end h2 em{font-style:normal;color:var(--accent)}
.end .row{display:flex;justify-content:center;margin-top:clamp(30px,5vh,54px)}
.end .row a{
  display:inline-flex;align-items:center;gap:10px;text-decoration:none;color:var(--ink-2);
  font-family:var(--mono);font-size:.78rem;letter-spacing:.08em;text-transform:uppercase;
  border:1px solid var(--line);background:var(--card);padding:11px 20px;border-radius:var(--r);
  transition:background-color var(--t-hover) ease,color var(--t-hover) ease,
             border-color var(--t-hover) ease,box-shadow var(--t-hover) ease,
             transform var(--t-press) var(--ease-out);
}
.end .row a:hover{background:var(--ink);color:var(--paper);border-color:var(--ink)}
.end .row svg{width:16px;height:16px;fill:currentColor}
/* The single call to action. It cannot reuse .act.solid: that is scoped to
   project cards, which define --a1 locally, and .end .row a would out-
   specify it anyway - the button would have come out as a pale outline
   with an invalid background. */
.end .row a.primary{background:var(--accent);border-color:var(--accent);color:var(--paper);font-weight:700}
.end .row a.primary:hover{background:var(--ink);border-color:var(--ink);color:var(--paper)}
/* ── footer ─────────────────────────────────────
   The site's foot, not the landing page's: it carries the
   same thing on all three pages, which is why the social
   links live HERE and no longer sit in a row directly above
   it. A footer that repeats the block it follows is filler.

   Two bands. The first is identity and routes; the second is
   the small print, separated by its own hairline so the
   copyright does not read as part of the navigation. */
.foot{border-top:1px solid var(--rule);margin-top:clamp(40px,7vh,90px)}
.foot-in{padding:clamp(38px,6vh,64px) var(--pad) clamp(26px,4vh,40px)}

.foot-top{
  display:flex;flex-wrap:wrap;align-items:start;
  gap:clamp(24px,5vw,64px);padding-bottom:clamp(26px,4vh,38px);
}
.foot-id{display:flex;flex-direction:column;gap:9px;margin-inline-end:auto}
.foot-id .mark{font-size:.8rem}
.foot-role{font-size:.9rem;color:var(--ink-2);max-width:34ch;line-height:1.5}

.foot-nav{display:flex;flex-direction:column;gap:11px;font-size:.9rem}
.foot-nav a{text-decoration:none;color:var(--ink-2);width:fit-content;transition:color var(--t-hover) ease}
.foot-nav a:hover{color:var(--accent-ink)}

/* icon plus word: at this size an icon on its own is a guessing game */
.foot-social{list-style:none;display:grid;grid-template-columns:repeat(2,auto);gap:11px clamp(16px,2.4vw,30px)}
.foot-social a{
  display:flex;align-items:center;gap:9px;text-decoration:none;
  color:var(--ink-2);font-size:.88rem;transition:color var(--t-hover) ease;
}
.foot-social svg{width:15px;height:15px;fill:currentColor;flex:none;opacity:.75;transition:opacity var(--t-hover) ease}
.foot-social a:hover{color:var(--ink)}
.foot-social a:hover svg{opacity:1}

.foot-legal{
  border-top:1px solid var(--rule);padding-top:clamp(16px,2.4vh,22px);
  display:flex;flex-wrap:wrap;align-items:center;gap:14px clamp(18px,3vw,36px);
  font-family:var(--mono);font-size:.68rem;color:var(--ink-3);letter-spacing:.02em;
}
/* the clock is real: his actual local time, so "open to work" has a
   timezone attached to it rather than being an abstract claim */
.foot-clock{display:flex;align-items:center;gap:7px;margin-inline-end:auto}
.foot-back{color:var(--ink-3);text-decoration:none;transition:color var(--t-hover) ease}
.foot-back:hover{color:var(--ink)}

/* Same reason the header has one: High Contrast forces border and
   background colours to system values, and these two hairlines are
   the only thing separating the footer from the page and the small
   print from the links. */
@media (forced-colors:active){
  .foot,.foot-legal{border-top-color:CanvasText}
}

@media(max-width:620px){
  .foot-top{gap:26px}
  .foot-id{margin-inline-end:0;flex-basis:100%}
  .foot-social{grid-template-columns:repeat(2,1fr)}
}

/* ════════════════════════════════════════════════════
   MOBILE & TOUCH
   Hover is a desktop luxury. On touch we trade it for
   scroll-into-view reveals and honest tap feedback.
   ════════════════════════════════════════════════════ */

@media(hover:none){
  /* Every hover that moves something now lives behind a pointer query at the
     end of the sheet, so there is no longer a list of hover effects to undo
     here — which is what used to go stale. Scrolling into view carries the
     same states instead. */
  .layer.lit{border-color:var(--lc)}
  .layer.lit::before{transform:scaleY(1)}

  /* tap feedback the pressable elements share with the desktop press state */
  .b:active{transform:scale(.99)}
}

/* touch targets never below 44px */
@media(pointer:coarse){
  .hero-links a{width:46px;height:46px}
  .act{padding:14px 22px}
  .b-list a{padding:16px 0}
  .end .row a{padding:14px 22px}
}

@media(max-width:760px){
  /* type: tighten the ladder so long words stop colliding */
  .head h1,.head h2{font-size:clamp(1.9rem,9vw,2.6rem);line-height:1}
  .proj h2{font-size:clamp(1.8rem,8.4vw,2.6rem)}
  .end h2{font-size:clamp(2rem,11vw,3.4rem);line-height:.98}
  .b-say h2{font-size:clamp(1.45rem,6vw,1.9rem)}
  .stat b{font-size:clamp(2.1rem,11vw,3rem)}

  /* rhythm: less air between blocks, more inside them */
  .proj{padding:clamp(46px,7vh,70px) 0}
  .head{padding:clamp(52px,8vh,80px) 0 clamp(22px,3vh,32px)}
  section{padding:60px 0}
  .bento-wrap{padding:clamp(44px,7vh,70px) 0}
  .spec div{padding:12px}
  .spec dd{font-size:.82rem}

  /* the hero name must never push the page sideways */
  .hero-name{font-size:clamp(2.3rem,12.5vw,4.2rem)}
  /* Anchored to the top on a phone, not the bottom. The text column is the
     full width here, so a bottom-anchored portrait guarantees a collision;
     moving it up gives the name and the meta line clean paper to sit on. */
  .hero-portrait{
    width:min(68vw,300px);height:min(44svh,370px);
    top:70px;bottom:auto;
  }
  /* On a phone the text column is the full width, so it lands ON the portrait
     instead of beside it. Dissolve the lower half of the photograph into the
     paper - the face still reads, and the words below it are on paper rather
     than on a dark suit. */
  .hero-portrait img{
    -webkit-mask-image:
      linear-gradient(90deg,transparent,#000 12%,#000 88%,transparent),
      linear-gradient(180deg,transparent,#000 10%,#000 62%,transparent);
    mask-image:
      linear-gradient(90deg,transparent,#000 12%,#000 88%,transparent),
      linear-gradient(180deg,transparent,#000 10%,#000 62%,transparent);
  }
  /* and a paper halo on the small type, for the part that still overlaps */
  .hero-idx,.hero-role{
    text-shadow:0 0 14px var(--paper),0 0 6px var(--paper),0 1px 2px var(--paper);
  }
  .hero-meta{gap:18px}
  .hero-role{max-width:100%}

  /* journey: photo first reads better than a stranded caption */
  .step{gap:14px}
  .step figure,.step .shots{order:-1}

  /* two videos side by side is unreadable at this width */
  .duo{grid-template-columns:1fr}
}

@media(max-width:420px){
  .chips{gap:5px}
  .chip{font-size:.66rem;padding:4px 9px}
  .acts{gap:8px}
  .act{width:100%;justify-content:center}
  .spec{grid-template-columns:1fr}
  .avail{font-size:.62rem}
}

.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}

/* ════════════════════════════════════════════════════
   SURFACES — last in the sheet so they win over the
   component defaults defined above.
   ════════════════════════════════════════════════════ */
#nav{background:rgba(241,228,209,.88)}
#nav.stuck{border-bottom-color:rgba(28,24,21,.2)}
.spec div{background:rgba(28,24,21,.045)}
.chip{background:rgba(28,24,21,.05)}
.layer{background:rgba(28,24,21,.04)}
.hero-links a,.globe,.end .row a{background:rgba(28,24,21,.05)}

/* brand colour travels as a variable, so it is never out-specified */
.s-github   {--brand:#1c1815}
.s-linkedin {--brand:#0a66c2}
.s-instagram{--brand:#e1306c}
.s-x        {--brand:#1c1815}
.s-telegram {--brand:#26a5e4}
.s-mail     {--brand:#ea4335}
.hero-links svg,.end .row svg{fill:var(--brand,currentColor)}
.hero-links a{border-color:color-mix(in srgb,var(--brand,#1c1815) 30%,transparent)}
.hero-links a:hover,.end .row a:hover{
  border-color:var(--brand,#1c1815);
  background:color-mix(in srgb,var(--brand,#1c1815) 12%,transparent);
}
.end .row a:hover{color:var(--ink)}

/* footer was set in tiny mono — make it readable */


/* ════════════════════════════════════════════════════
   ARABIC · RTL
   Layout mirrors via dir=rtl and logical properties; only
   transforms, decorative arrows and type need explicit help.
   ════════════════════════════════════════════════════ */

[lang="ar"]{
  font-family:var(--sans-ar);
  /* Arabic needs a little more line height and no letter-spacing */
  line-height:1.85;
}
[lang="ar"] .hero-name,
[lang="ar"] .head h1,
[lang="ar"] .head h2,
[lang="ar"] .proj h2,
[lang="ar"] .post h2,
[lang="ar"] .end h2{
  font-family:var(--display-ar);
  letter-spacing:0;
  text-transform:none;          /* Arabic has no letter case */
}
[lang="ar"] .hero-name{line-height:1.15;font-size:clamp(2.2rem,8vw,7.2rem)}
/* Arabic sits taller than Latin — the damma on بُنيت needs headroom,
   so stacked lines get a little more air between them. 1.42 still let the
   hamza of وأُطلقت crowd the line above, hence 1.62. */
[lang="ar"] .proj h2,[lang="ar"] .head h1,[lang="ar"] .head h2,[lang="ar"] .end h2,[lang="ar"] .post h2{line-height:1.62}
[lang="ar"] .eyebrow,[lang="ar"] .tag,[lang="ar"] .spec dt,
[lang="ar"] .hero-idx,[lang="ar"] .avail,[lang="ar"] .stat small,
[lang="ar"] .b-list h3,[lang="ar"] .b-list a span:last-child,
[lang="ar"] .b-lang small,[lang="ar"] .end .row a,[lang="ar"] .post-body summary,
/* the footer arrived after this list was written, and its legal band holds
   the city and the back-to-top link - both Arabic, both previously set in
   JetBrains Mono, which has no Arabic glyphs at all, with letter-spacing
   that pulls apart letters the script requires to be joined */
[lang="ar"] .foot-legal,[lang="ar"] .foot-clock,[lang="ar"] .foot-back,
[lang="ar"] .foot-nav a,[lang="ar"] .foot-role{
  font-family:var(--sans-ar);letter-spacing:0;text-transform:none;
}
/* Arabic has no italic — carry the emphasis with weight and colour instead */
[lang="ar"] .b-say h2,[lang="ar"] .bento-head .who,[lang="ar"] .b-cta .t{
  font-family:var(--display-ar);font-style:normal;
}
[lang="ar"] .b-say h2 em,[lang="ar"] .end h2 em{font-style:normal;font-weight:700}
/* Cyrillic: Instrument Serif is Latin-only, so Russian falls back cleanly */
[lang="ru"] .b-say h2,[lang="ru"] .bento-head .who,[lang="ru"] .b-cta .t{
  font-family:Georgia,'Times New Roman',serif;
}

/* ── mirrored transforms (logical properties can't reach these) ──
   Resting states only. The mirrored *hover* states sit with the rest of the
   hover motion at the foot of the sheet, so they stay inside the pointer
   query instead of leaking back out of it. */
[dir="rtl"] .b-cta .arw{transform:scaleX(-1)}

/* the hero hairline flips with the stagger */
[dir="rtl"] .hero-name .l2::before{background:var(--ink)}

/* mobile stagger already uses a physical margin — mirror it */
@media(max-width:640px){
}
@media(max-width:860px){
}

/* ════════════════════════════════════════════════════
   POINTER TILT
   While the cursor is being tracked the card follows on
   a short linear tween. The long curve stays for the
   release, so tracking is tight and the return is soft —
   re-firing a half-second ease on every pointer event
   just made the card float somewhere behind the cursor.
   ════════════════════════════════════════════════════ */
/* 120ms linear tracked the pointer almost exactly, which reads as the card
   snapping about rather than following. A longer, eased tween lags the
   cursor just enough to feel like weight instead of twitch. */
.b.tilting,[data-tilt].tilting,.step figure.tilting{transition:transform 260ms var(--ease-out)}

/* ════════════════════════════════════════════════════
   HOVER MOTION — gated, and last so it wins
   Touch fires :hover on tap and then leaves it stuck.
   Everything that *moves* on hover lives here instead of
   being declared globally and undone for touch further
   up, which is the list that kept going stale. Colour
   and border hovers stay with their components.
   ════════════════════════════════════════════════════ */
@media(hover:hover) and (pointer:fine){
  .hero-links a:hover{transform:translateY(-3px)}
  .act:hover,.end .row a:hover{transform:translateY(-2px)}
  .b:hover{box-shadow:0 26px 60px -22px rgba(28,24,21,.18)}
  .b-cta:hover .arw{transform:translate(5px,-5px)}
  .b-list a:hover{translate:8px}
  .layer:hover{
    border-color:var(--lc);transform:translateX(10px) translateZ(26px) rotateY(-1.6deg);
    box-shadow:-14px 18px 40px -26px rgba(28,24,21,.16);
  }
  .layer:hover::before{transform:scaleY(1)}
  .vis:hover .shot video,.vis:hover .shot img:not([data-par]){transform:scale(1.04)}
  .step:hover figure img,.step:hover figure video{filter:grayscale(0);transform:scale(1.03)}

  /* the same states, mirrored */
  [dir="rtl"] .b-cta:hover .arw{transform:scaleX(-1) translate(5px,-5px)}
  [dir="rtl"] .b-list a:hover{translate:-8px}
  [dir="rtl"] .layer:hover{transform:translateX(-10px) translateZ(26px) rotateY(1.6deg)}
}

/* ════════════════════════════════════════════════════
   PRESS
   Feedback belongs on the press, and not only on touch —
   a click on a desktop used to get nothing back at all.
   Last in the sheet so it beats the hover state it
   interrupts.
   ════════════════════════════════════════════════════ */
.act:active,.globe:active,
.end .row a:active,.hero-links a:active,.lang-pop :is(a,button):active{
  transform:scale(.97);
}

/* ════════════════════════════════════════════════════
   WORK RIBBON
   The project screenshots as one continuous sheet that
   folds back on itself as you scroll through this block.

   It borrows the <img> elements already used by the
   project sections below, so there is no duplicated
   markup, no second download and no repeated alt text.
   The block is decorative — everything it shows is
   presented properly further down the page — so it is
   aria-hidden, and if WebGL or motion is unavailable it
   simply collapses to nothing and the page reads as it
   always did.

   Height is set in JS: it is the scroll budget the fold
   needs, so it has to follow the strip length.
   ════════════════════════════════════════════════════ */
/* pulled up into the heading's trailing space so the sheets sit close to the
   copy rather than a screen below it */
.ribbon{position:relative;display:none;margin-top:-10vh}
.ribbon.on{display:block}
.ribbon-gl{position:sticky;top:0;display:block;width:100%;height:100svh;pointer-events:none}
@media(prefers-reduced-motion:reduce){.ribbon,.ribbon.on{display:none}}

/* ════════════════════════════════════════════════════
   FOCUS
   This sheet had no :focus rule anywhere - every keyboard
   user was getting the browser default. That default also
   gets clipped inside .b and .shot, which are overflow:
   hidden, so the ring is inset there instead of outset.
   The fill accent is the right token here, not the text
   one: at 4.56:1 on paper it clears the 3:1 non-text bar
   with room, and it stays the brand colour.
   ════════════════════════════════════════════════════ */
:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:var(--r)}
.b :focus-visible,.shot :focus-visible,.ticker :focus-visible{outline-offset:-3px}
@supports not selector(:focus-visible){
  a:focus,button:focus,summary:focus{outline:2px solid var(--accent);outline-offset:2px}
}
