/* ============================================================
   Editorial Swiss Minimal — variant A
   Near-white ground · near-black ink · one ultramarine accent
   Works panels cycle three surfaces: that ground, vermilion, ink
   Display / Text / Labels: Jost — geometric minimal gothic (Futura spirit)
   ============================================================ */

/* ---------- self-hosted type (Jost variable, latin) ---------- */
@font-face {
  font-family: 'Jost';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url('../fonts/jost.woff2') format('woff2');
}

/* ---------- design tokens ---------- */
:root {
  --paper:   #F6F6F6;
  /* the cover mat, and the one thing that must stay brighter than every panel
     surface: a cover reads as a print laid on the panel, not a hole in it.
     With the page at #F6F6F6 that leaves exactly one step — pure white. */
  --plate:   #FFFFFF;
  --ink:     #17150E;
  --ink-2:   #4C483B;
  --ink-3:   #6B6656;
  --line:    rgba(23, 21, 14, 0.14);
  --line-2:  rgba(23, 21, 14, 0.28);
  --accent:  #122AD6;          /* International Klein Blue spirit */
  --accent-deep: #0B1C9C;
  --ikb:     #122AD6;          /* fixed site accent for card numbers/status (plates set --accent per project) */

  --shell: 1240px;
  --pad: clamp(1.25rem, 5vw, 5rem);
  --gap: clamp(2.4rem, 4.5vw, 4.75rem);

  --f-display: 'Jost', 'Futura', 'Avenir Next', 'Helvetica Neue', sans-serif;
  --f-text: 'Jost', 'Futura', 'Avenir Next', 'Helvetica Neue', sans-serif;
  --f-label: 'Jost', 'Futura', 'Avenir Next', 'Helvetica Neue', sans-serif;

  --ease: cubic-bezier(0.22, 0.68, 0.16, 1);
  /* hover answers fast — it is the most frequent interaction on a desktop
     portfolio; --dur-ui is the modal's exit and the case crossfade (2026-09-02 motion audit) */
  --dur-hover: 240ms;
  --dur-ui: 200ms;

  /* ---- type scale ----------------------------------------------------
     the 3 label steps and 2 text steps fold 13 fixed values (0.64–1.06rem)
     into the fewest steps that keep every value within ±0.04rem of what it
     was. min is the current value (narrow screens are unchanged); it grows
     by +0.04rem only on wide screens. the ten below just name the existing
     clamp()s — no values changed. */
  --text-3xs: clamp(0.68rem, calc(0.665rem + 0.061vw), 0.72rem);
  --text-2xs: clamp(0.72rem, calc(0.705rem + 0.061vw), 0.76rem);
  --text-xs:  clamp(0.82rem, calc(0.805rem + 0.061vw), 0.86rem);
  --text-sm:  clamp(0.98rem, calc(0.965rem + 0.061vw), 1.02rem);
  --text-md:  clamp(1.06rem, calc(1.045rem + 0.061vw), 1.1rem);

  --text-base: clamp(1rem, 0.96rem + 0.2vw, 1.12rem);
  --text-hero-name: clamp(2.4rem, 7.8vw, 7.2rem);
  --text-hero-role: clamp(0.82rem, 1.4vw, 0.95rem);
  --text-hero-tag: clamp(1.1rem, 2.3vw, 1.7rem);
  --text-sec-title: clamp(1.6rem, 3.6vw, 2.7rem);
  --text-plate-title: clamp(1.45rem, 2.2vw, 1.9rem);
  --text-about-lead: clamp(1.6rem, 4.2vw, 3rem);
  --text-footer-mail: clamp(1.15rem, 2vw, 1.55rem);
  --text-modal-title: clamp(2rem, 5vw, 3.1rem);
  --text-modal-tagline: clamp(1.1rem, 2.4vw, 1.4rem);

  /* ---- cut corner ----------------------------------------------------
     this site has no border-radius anywhere, so avax's rounded notch is
     translated into a hard diagonal cut instead. the inner shape is
     always 1px smaller. */
  --cut: clamp(0.9rem, 2.2vw, 1.6rem);
  --cut-sm: 0.5rem;
}

/* ---------- reset-ish ---------- */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; scrollbar-color: var(--ink-3) transparent; scrollbar-width: thin; }
@media (prefers-reduced-motion: no-preference) { html { scroll-behavior: smooth; } }
body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--f-text);
  font-size: var(--text-base);
  line-height: 1.62;
  font-weight: 400;
  overflow-x: hidden;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
}
img, svg { max-width: 100%; display: block; }
h1, h2, h3, h4, p, ul, ol, figure { margin: 0; }
ul, ol { list-style: none; padding: 0; }
a { color: inherit; text-decoration: none; }
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }

/* film grain — barely-there tactility over the paper */
body::after {
  content: "";
  position: fixed; inset: 0; pointer-events: none; z-index: 0;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 160px 160px;
  opacity: 0.05; mix-blend-mode: multiply;
}

/* ---------- selection / focus / scrollbar ----------
   Both take --ikb rather than --accent. --accent is the per-project colour and
   can be light (one plate's gold puts a selection highlight at 2.1:1), and inside a
   discipline panel it is set inline on the plate, out of the panel's reach.
   --ikb is flipped together with --paper by every panel, so this pair stays
   legible on all three surfaces: 8.3:1 on the ground, 5.9:1 on vermilion and
   6.9:1 on ink. A focus ring is furniture, not an accent pop — the plate's hover twin
   and title underline still carry the project colour. */
::selection { background: var(--ikb); color: var(--paper); }
:focus-visible { outline: 2px solid var(--ikb); outline-offset: 4px; border-radius: 2px; }
::-webkit-scrollbar { width: 12px; height: 12px; }
::-webkit-scrollbar-track { background: var(--paper); }
::-webkit-scrollbar-thumb {
  background: var(--ink-3); border: 3px solid var(--paper); border-radius: 99px;
}
::-webkit-scrollbar-thumb:hover { background: var(--ink-2); }

.skip {
  position: fixed; top: 0; left: 0; transform: translateY(-120%);
  background: var(--ink); color: var(--paper); padding: 0.7rem 1.1rem;
  z-index: 100; font-family: var(--f-label); font-weight: 500; font-size: var(--text-xs); border-radius: 0 0 6px 0;
  transition: transform 0.2s var(--ease);
}
.skip:focus { transform: translateY(0); }

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

/* mono kicker used across sections */
.kicker {
  font-family: var(--f-label); font-weight: 500; font-size: var(--text-2xs);
  letter-spacing: 0.22em; text-transform: uppercase; color: var(--ink-2);
  display: inline-flex; align-items: center; gap: 0.7rem;
}
.kicker::before {
  content: ""; width: 1.9rem; height: 1px; background: var(--ink-2); display: inline-block;
}

/* ============================================================
   HEADER
   ============================================================ */
.site-header {
  position: sticky; top: 0; z-index: 40;
  backdrop-filter: saturate(1.4) blur(9px);
  -webkit-backdrop-filter: saturate(1.4) blur(9px);
  background: color-mix(in srgb, var(--paper) 82%, transparent);
  border-bottom: 1px solid var(--line);
}
.site-header__in {
  display: flex; align-items: center; justify-content: space-between;
  gap: 1rem; padding-block: 0.85rem;
}
.site-header__lead { display: flex; align-items: center; gap: clamp(0.8rem, 2vw, 1.4rem); }

/* availability signal — green dot + tracked label (header + footer) */
.avail {
  display: inline-flex; align-items: center; gap: 0.5rem;
  font-family: var(--f-label); font-weight: 500; font-size: var(--text-3xs);
  letter-spacing: 0.16em; text-transform: uppercase; color: var(--ink-2);
}
.avail[hidden] { display: none; }
.avail__dot {
  width: 0.5rem; height: 0.5rem; border-radius: 50%; flex-shrink: 0;
  background: #1F7A46;
}
/* hide the header signal before it crowds the logo + nav */
@media (max-width: 720px) { .avail--header { display: none; } }
.logo {
  font-family: var(--f-display); font-weight: 500; font-size: var(--text-sm);
  letter-spacing: 0.14em; text-transform: uppercase; display: inline-flex; align-items: center; gap: 0.5rem;
  line-height: 1;
}
.logo__dot { width: 0.5rem; height: 0.5rem; background: var(--accent); border-radius: 50%; }
@media (hover: hover) and (pointer: fine) {
  .logo:hover .logo__dot { transform: scale(1.35); transition: transform var(--dur-hover) var(--ease); }
}
.nav { display: flex; gap: clamp(1rem, 2.5vw, 2.4rem); align-items: center; }
.nav a {
  font-family: var(--f-label); font-weight: 500; font-size: var(--text-2xs); letter-spacing: 0.16em;
  text-transform: uppercase; color: var(--ink-2); position: relative; padding-block: 0.3rem;
}
.nav a::after {
  content: ""; position: absolute; left: 0; bottom: 0; height: 1.5px; width: 100%;
  background: var(--accent); transform: scaleX(0); transform-origin: left;
  transition: transform var(--dur-hover) var(--ease);
}
@media (hover: hover) and (pointer: fine) {
  .nav a:hover { color: var(--ink); }
  .nav a:hover::after { transform: scaleX(1); }
}
@media (max-width: 560px) { .nav a:first-child { display: none; } }

/* ============================================================
   HERO
   ============================================================ */
.hero { padding-top: clamp(2rem, 4.5vw, 4rem); padding-bottom: clamp(1rem, 2.5vw, 2rem); }
.hero__grid {
  display: grid; grid-template-columns: repeat(12, 1fr);
  gap: clamp(0.75rem, 2vw, 1.4rem) 1rem; align-items: end;
}
.hero__kicker { grid-column: 1 / -1; margin-bottom: clamp(1.2rem, 3vw, 2.4rem); }
.hero__name {
  grid-column: 1 / -1;
  font-family: var(--f-display); font-weight: 800;
  font-size: var(--text-hero-name);
  line-height: 0.94; letter-spacing: -0.015em;
  text-transform: uppercase; overflow-wrap: break-word;
  will-change: transform;
  margin-bottom: clamp(1.5rem, 3.5vw, 2.6rem);
}
.hero__role {
  grid-column: 1 / 7; align-self: start;
  font-family: var(--f-label); font-weight: 500; font-size: var(--text-hero-role);
  letter-spacing: 0.06em; text-transform: uppercase; color: var(--accent);
  padding-top: 0.4rem;
}
.hero__tag {
  grid-column: 7 / -1;
  font-family: var(--f-text);
  font-size: var(--text-hero-tag); line-height: 1.38; color: var(--ink-2);
  max-width: 34ch; text-wrap: balance;
}
.hero__scroll {
  grid-column: 1 / -1; margin-top: clamp(1rem, 2.5vw, 1.85rem);
  display: inline-flex; align-items: center; gap: 0.6rem; width: fit-content;
  font-family: var(--f-label); font-weight: 500; font-size: var(--text-2xs); letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--ink-2);
}
.hero__scroll .arrow {
  display: inline-block; transition: transform var(--dur-hover) var(--ease);
}
@media (hover: hover) and (pointer: fine) {
  .hero__scroll:hover { color: var(--ink); }
  .hero__scroll:hover .arrow { transform: translateY(4px); }
}
@media (max-width: 720px) {
  .hero__role { grid-column: 1 / -1; margin-bottom: 0.6rem; }
  .hero__tag { grid-column: 1 / -1; }
}

/* ============================================================
   CAPABILITIES TICKER — signature strip in the hero-to-works band
   ============================================================ */
/* A departure board: an ink panel with the capabilities running across it in
   paper-coloured cells. Same palette as the rest of the page — the board reads
   as printed rather than lit, so it sits in the paper world instead of
   fighting it. */
.ticker {
  position: relative; z-index: 1; overflow: hidden;
  border-block: 1px solid var(--ink);
  background: var(--ink);
  /* a hairline of paper along both edges, so the board reads as a plate
     mounted on the page rather than a hole cut into it */
  box-shadow: inset 0 1px 0 color-mix(in srgb, var(--paper) 26%, transparent),
              inset 0 -1px 0 color-mix(in srgb, var(--paper) 26%, transparent);
  -webkit-mask-image: linear-gradient(to right, transparent, #000 7%, #000 93%, transparent);
  mask-image: linear-gradient(to right, transparent, #000 7%, #000 93%, transparent);
}
.ticker__track {
  display: flex; width: max-content;
  /* The keyframe moves the track by half its OWN width, so the distance
     travelled grows with the number of copies — a fixed duration would speed
     the band up every time one is added, and adding four took it from 34.2 to
     102.5 px/s. Per-copy seconds keeps the speed wherever the count goes:
     23s x copies always comes out at 34.2 px/s, the rate the band was set to.
     --copies has to match the number of .ticker__group elements in the HTML. */
  --copies: 6;
  animation: ticker-scroll calc(23s * var(--copies)) linear infinite;
  /* No cell grid. A dot mask was tried and dropped: at this label size the
     dots are finer than the strokes, so they never read as cells — they only
     dim the type. Enlarging the type until the cells showed changed the label
     into a headline, which is not what this strip is. */
}
.ticker__group { display: flex; flex-shrink: 0; align-items: center; padding-block: 1.35rem; }
.ticker__item {
  font-family: var(--f-label); font-weight: 500; font-size: var(--text-2xs); letter-spacing: 0.14em; text-transform: uppercase;
  /* the last step above the ink, and a literal on purpose — --plate happens to
     share this value but a cover mat and board type are separate ideas, and this
     must not follow if the mat moves. Board type is read at a glance while it
     moves, so it takes that last step: 18.3:1, against 16.9:1 for --paper. */
  color: #fff; white-space: nowrap;
  display: inline-flex; align-items: center; padding-inline: clamp(0.9rem, 2vw, 1.6rem);
}
.ticker__item::before {
  content: ""; width: 0.34rem; height: 0.34rem; margin-right: 0.7rem; flex-shrink: 0;
  background: var(--accent); transform: rotate(45deg);
}
@keyframes ticker-scroll {
  from { transform: translateX(0); }
  to { transform: translateX(-50%); }
}
@media (hover: hover) and (pointer: fine) {
  .ticker:hover .ticker__track { animation-play-state: paused; }
}
@media (prefers-reduced-motion: reduce) {
  .ticker__track { animation: none; transform: none; }
}

/* section head */
.sec-head {
  display: flex; align-items: flex-end; justify-content: space-between;
  gap: 1.5rem; flex-wrap: wrap;
  border-top: 1.5px solid var(--ink); padding-top: 1.1rem; margin-bottom: clamp(1.25rem, 2.5vw, 2rem);
}
.sec-head__title {
  font-family: var(--f-display); font-weight: 500; text-transform: uppercase;
  font-size: var(--text-sec-title); letter-spacing: 0.02em; line-height: 1.05;
}
.sec-head__meta { font-family: var(--f-label); font-weight: 500; font-size: var(--text-2xs); color: var(--ink-2); letter-spacing: 0.05em; }

/* ============================================================
   WORKS
   ============================================================ */
.works { padding-top: clamp(1rem, 2vw, 1.75rem); padding-bottom: clamp(2.5rem, 6vw, 5rem); }
.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;
}

/* ---- discipline panels ------------------------------------------------
   The panels stack: each sticks one band lower than the one before it, so
   the bands pile up at the top while the covers scroll away underneath.
   min-height rather than height on purpose — if a short viewport cannot
   fit a panel, it grows and scrolls normally instead of clipping its own
   cards. Below 1024px the stacking is off entirely: three covers and a
   band do not fit one phone screen. */
.stack { display: flex; flex-direction: column; }
/* Spacer between two stacked panels. Its height — written by syncStackMetrics —
   is pure scroll distance before the next panel arrives: panel heights, bands,
   offsets and the shared release point never see it. An hr rather than a div so
   the nth-of-type colour cycle below cannot count it. Hidden wherever the stack
   itself is off (narrow windows; print falls below the width query). */
.stage-gap { display: none; border: 0; margin: 0; }
.disc {
  background: var(--paper);
  color: var(--ink);
  border-top: 1.5px solid var(--ink);
  padding-top: 1.1rem;
  padding-bottom: clamp(2rem, 4vw, 3.5rem);
}
/* Three committed surfaces rather than three tints of one: paper, vermilion,
   ink. A dark panel redefines the paper/ink token pair for everything inside
   it, so the head, numbers, captions, rules and the View chip invert together
   instead of being recoloured one by one — and the `background: var(--paper)`
   above then paints the panel with no second declaration. --plate stays out of
   the flip on purpose: the cover mat is bright on every panel, so a cover reads
   as a framed print laid on colour. nth-of-type so a future non-panel sibling
   cannot shift the cycle, and so a fourth discipline starts the three over. */
.disc:nth-of-type(3n+2) { --paper: #B7231D; }   /* vermilion — the one colour from outside the palette */
.disc:nth-of-type(3n)   { --paper: #17150E; }   /* the ink, used as a surface */
.disc:nth-of-type(3n+2),
.disc:nth-of-type(3n) {
  /* The rule along the top edge exists to separate two sheets of the same
     paper. A change of surface already separates them, and the flip would draw
     that rule in the panel's light ink — a pale seam, most visible where ink
     meets vermilion. Transparent rather than removed so the band keeps the
     same 1.5px it is measured with. */
  border-top-color: transparent;
  --ink:    #F6F6F6;
  /* Both alphas are set by vermilion, the lighter of the two surfaces, and both
     clear 4.5:1 on it — 5.2:1 and 4.9:1 measured, where the 0.76 they started
     from gives 4.0:1. Holding --ink-3 to the same bar costs nothing today (no
     panel element uses it) and means adding one later cannot fail. On ink they
     only read cleaner: 14.4:1 and 13.2:1. */
  --ink-2:  rgba(246, 246, 246, 0.92);
  --ink-3:  rgba(246, 246, 246, 0.88);
  --line:   rgba(246, 246, 246, 0.22);
  --line-2: rgba(246, 246, 246, 0.4);
  /* the site accent is a blue: on vermilion it clashes, on ink it sinks. Both
     panels hand numbers and status to their own ink; ink takes the accent back
     below, lifted far enough to read. */
  --ikb: var(--ink);
}
/* on ink the accent survives, lifted toward the panel's own ink until it
   clears 4.5:1 against it (measured 6.9:1) */
.disc:nth-of-type(3n) { --ikb: color-mix(in srgb, var(--accent) 45%, var(--ink)); }
/* The two hover decorations still carry the per-project accent — that pop is
   the point of them — but the accent is a dark ink, so on a dark panel they
   would sink into the surface. There they draw in the panel's own ink instead.
   (Selection and focus rings do not need an entry here: they moved to --ikb,
   which every panel flips.) */
.disc:nth-of-type(3n+2) .plate__frame::after,
.disc:nth-of-type(3n) .plate__frame::after { border-color: var(--ink); }
.disc:nth-of-type(3n+2) .plate__title .sweep,
.disc:nth-of-type(3n) .plate__title .sweep { background-image: linear-gradient(var(--ink), var(--ink)); }
/* A hairline drawn ON the mat must not flip with the panel: --plate stays bright
   everywhere, so a light rule on it disappears (measured 1.03:1, against 1.9:1
   for the root value kept here). This is the only rule in that position — the
   mat's outer 1px sits on the panel instead, where flipping is correct. */
.disc:nth-of-type(3n+2) .plate__cover--shot .cover-img,
.disc:nth-of-type(3n) .plate__cover--shot .cover-img { border-color: rgba(23, 21, 14, 0.28); }
.disc__head {
  display: grid; grid-template-columns: auto 1fr auto;
  align-items: baseline; column-gap: 1rem;
  padding-bottom: clamp(1rem, 2vw, 1.6rem);
}
.disc__no {
  font-family: var(--f-label); font-weight: 500; font-size: var(--text-xs);
  color: var(--ikb); font-variant-numeric: tabular-nums;
}
.disc__title {
  font-family: var(--f-display); font-weight: 500; font-size: var(--text-plate-title);
  letter-spacing: 0.02em; line-height: 1.05;
}
.disc__meta {
  font-family: var(--f-label); font-weight: 500; font-size: var(--text-2xs);
  color: var(--ink-2); letter-spacing: 0.05em;
}

@media (min-width: 1024px) {
  /* covers abreast inside a panel: the more a discipline holds, the narrower
     its covers — but always one row. A second row cannot work here: a pinned
     panel taller than the viewport keeps its overflow below the fold for ever,
     and the next panel then covers cards nobody was ever shown (measured at
     1280×900 when Web Apps reached four). 5+ has no rule and would wrap —
     add a span here before a discipline grows past four. */
  .disc .plates { gap: clamp(2rem, 3vw, 3rem) 2rem; }
  .disc .plates--4 .plate { grid-column: span 3; }
  .disc .plates--3 .plate { grid-column: span 4; }
  .disc .plates--2 .plate { grid-column: span 6; }
  .disc .plates--1 .plate { grid-column: span 6; }
}

/* The stack pins only when the window is wide AND tall enough. Width alone
   was the original gate, and a wide-but-shallow window (a 530px-tall browser
   on a 13-inch screen) then paid ~212px of header+bands for ~320px of stage —
   the two-across plates could never fit the pinned frame and their captions
   stayed below the fold at every scroll position (external audit, round 1:
   the one open HIGH). Below either threshold the panels flow normally, so
   nothing can permanently occlude them; the height twin of the 1024px rule. */
@media (min-width: 1024px) and (min-height: 640px) {
  /* Two floors, whichever is taller. The viewport one lines the panel bottoms
     up on a tall window. --floor is the one that keeps the stacking honest: a
     panel has to stay at least its own band taller than the panel below it, or
     that panel reaches it before its band has scrolled away and covers it. On
     a short window a panel whose content overflows the viewport floor breaks
     that chain, so JS measures it back-to-front and writes the floor here. */
  .disc {
    position: sticky;
    top: calc(var(--header-h, 4rem) + var(--offset, 0px));
    min-height: max(calc(100vh - var(--header-h, 4rem) - var(--offset, 0px)), var(--floor, 0px));
    min-height: max(calc(100lvh - var(--header-h, 4rem) - var(--offset, 0px)), var(--floor, 0px));
  }
  .stage-gap { display: block; }
}

/* The column gap starts at zero because below 720px every plate spans all
   twelve tracks — nothing is ever beside anything, so the gap separates a plate
   from itself. It was not free, though: eleven 2rem gaps put a 352px floor
   under the grid, and a 320px screen only has a 280px shell to give it, so the
   page scrolled sideways by 52px. The gap comes back with the two-up. */
.plates {
  display: grid; grid-template-columns: repeat(12, 1fr);
  gap: clamp(3.5rem, 6vw, 5.5rem) 0;
}
@media (min-width: 720px) { .plates { column-gap: 2rem; } }
.plate { grid-column: span 12; }
/* 2-up on the 12-col axis from 720px up — stays 2-up at large sizes for gallery-like plates */
@media (min-width: 720px) { .plate { grid-column: span 6; } }

.plate[hidden] { display: none; }
.plate__btn {
  display: block; width: 100%; text-align: left; padding: 0;
  -webkit-tap-highlight-color: transparent;
}
.plate__btn:focus-visible { outline: 2px solid var(--ikb); outline-offset: 8px; }

.plate__frame { position: relative; display: block; }
/* registration-shift plate: an accent duplicate slides out on hover */
.plate__frame::after {
  content: ""; position: absolute; inset: 0; z-index: -1;
  border: 1.5px solid var(--accent);
  clip-path: polygon(0 0, calc(100% - var(--cut)) 0, 100% var(--cut), 100% 100%, 0 100%);
  opacity: 0; transform: translate(0, 0);
  transition: transform 0.35s var(--ease), opacity var(--dur-hover) var(--ease);
}
/* the cut-corner frame: the 1px padding is the hairline, and the background
   is its colour — clip-path will not carry a border around the diagonal */
.plate__cover {
  position: relative; display: block; padding: 1px;
  background: var(--line-2);
  aspect-ratio: 3 / 2;
  clip-path: polygon(0 0, calc(100% - var(--cut)) 0, 100% var(--cut), 100% 100%, 0 100%);
}
/* the paper inside the frame — one step smaller so the hairline stays visible
   along the diagonal too */
.mat {
  display: block; width: 100%; height: 100%; background: var(--plate);
  clip-path: polygon(0 0, calc(100% - var(--cut) + 1px) 0, 100% calc(var(--cut) - 1px), 100% 100%, 0 100%);
}
/* real screenshots sit on a warm mat with a hairline frame, like a print —
   matching the two 'contain' covers and the inset SVG art */
.plate__cover--shot .mat { padding: clamp(0.9rem, 2vw, 1.4rem); }
.plate__cover--shot .cover-img {
  border: 1px solid var(--line-2); object-fit: cover; width: 100%; height: 100%;
}
.plate__cover .cover-svg {
  width: 100%; height: 100%;
  filter: saturate(0.86) sepia(0.03);
  transition: transform 0.35s var(--ease), filter var(--dur-hover) var(--ease);
}
/* hover-only clickability cue; cover stays a clean product shot at rest */
.plate__view {
  position: absolute; right: 0.7rem; bottom: 0.7rem; z-index: 2;
  font-family: var(--f-label); font-weight: 500; font-size: var(--text-3xs); letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--paper); background: var(--ink);
  padding: 0.34rem 0.62rem; display: inline-flex; align-items: center; gap: 0.4rem;
  opacity: 0; transform: translateY(6px);
  transition: opacity var(--dur-hover) var(--ease), transform var(--dur-hover) var(--ease);
  clip-path: polygon(0 0, calc(100% - var(--cut-sm)) 0, 100% var(--cut-sm), 100% 100%, 0 100%);
}
.plate__view .ar { transition: transform var(--dur-hover) var(--ease); }
/* keyboard focus keeps the reveal; the hover twin lives in the hover media query below */
.plate__btn:focus-visible .plate__view { opacity: 1; transform: none; }
.plate__btn:focus-visible .plate__view .ar { transform: translate(2px, -2px); }
@media (hover: hover) and (pointer: fine) {
  .plate__btn:hover .plate__view { opacity: 1; transform: none; }
  .plate__btn:hover .plate__view .ar { transform: translate(2px, -2px); }
}

.plate__cap {
  display: grid; grid-template-columns: auto 1fr; column-gap: 1rem; align-items: baseline;
  padding-top: 1rem;
}
.plate__no {
  font-family: var(--f-label); font-weight: 500; font-size: var(--text-xs); color: var(--ikb);
  grid-row: 1 / 3; align-self: start; padding-top: 0.35rem;
  transition: color var(--dur-hover) var(--ease); font-variant-numeric: tabular-nums;
}
.plate__title {
  font-family: var(--f-display); font-weight: 500; font-size: var(--text-plate-title);
  letter-spacing: -0.01em; line-height: 1.08;
}
.plate__title .sweep {
  background-image: linear-gradient(var(--accent), var(--accent));
  background-repeat: no-repeat; background-position: 0 100%;
  background-size: 0% 2px; padding-bottom: 2px;
  transition: background-size var(--dur-hover) var(--ease);
}
.plate__foot {
  grid-column: 2; display: flex; align-items: baseline; flex-wrap: wrap;
  gap: 0.3rem 0.7rem; margin-top: 0.4rem;
}
.plate__cat {
  font-family: var(--f-label); font-weight: 500; font-size: var(--text-2xs); color: var(--ink-2); letter-spacing: 0.04em;
}
.plate__status {
  font-family: var(--f-label); font-weight: 500; font-size: var(--text-2xs); letter-spacing: 0.04em; color: var(--ikb);
  display: inline-flex; align-items: center; gap: 0.42rem;
}
.plate__status-dot { width: 0.4rem; height: 0.4rem; border-radius: 50%; background: var(--ikb); }

/* choreography — :focus-visible (keyboard) stays outside the media query;
   the :hover twin is gated so touch taps never leave a plate stuck "hovered" */
.plate__btn:focus-visible .plate__cover .cover-svg { transform: scale(1.025); filter: saturate(1); }
.plate__btn:focus-visible .plate__frame::after { opacity: 1; transform: translate(11px, 11px); }
.plate__btn:focus-visible .plate__title .sweep { background-size: 100% 2px; }
@media (hover: hover) and (pointer: fine) {
  .plate__btn:hover .plate__cover .cover-svg { transform: scale(1.025); filter: saturate(1); }
  .plate__btn:hover .plate__frame::after { opacity: 1; transform: translate(11px, 11px); }
  .plate__btn:hover .plate__title .sweep { background-size: 100% 2px; }
}

/* ============================================================
   ABOUT
   ============================================================ */
.about { padding-block: clamp(4rem, 9vw, 8rem); }
.about__grid {
  display: grid; grid-template-columns: repeat(12, 1fr); gap: clamp(1.5rem, 4vw, 3rem) 1rem;
}
.about__lead {
  grid-column: 1 / -1;
  font-family: var(--f-display); font-weight: 500;
  font-size: var(--text-about-lead); line-height: 1.12; letter-spacing: -0.01em;
  text-wrap: balance; max-width: 20ch;
}
/* highlighter sweep — reads as emphasis, not a link */
.about__lead .em {
  color: var(--ink);
  background: linear-gradient(transparent 60%, color-mix(in srgb, var(--accent) 26%, transparent) 60%);
  padding: 0 0.06em;
}
.about__body { grid-column: 1 / 8; font-size: var(--text-md); color: var(--ink-2); max-width: 52ch; }
.about__body p + p { margin-top: 1rem; }
.about__skills { grid-column: 9 / -1; align-self: start; }
.about__skills h3, .about__why h3 {
  font-family: var(--f-label); font-weight: 500; font-size: var(--text-2xs); letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--ink-3); margin-bottom: 1rem; padding-bottom: 0.6rem; border-bottom: 1px solid var(--line);
}
.about__skills ul { display: flex; flex-direction: column; }
.about__skills li {
  font-family: var(--f-label); font-weight: 500; font-size: var(--text-xs); color: var(--ink);
  padding: 0.5rem 0; border-bottom: 1px solid var(--line);
  display: flex; justify-content: space-between; align-items: baseline;
}

/* "why work with me" — same ruled language as the skills list, numbers lead */
.about__why { margin-top: clamp(1.75rem, 4vw, 2.5rem); max-width: 46ch; }
.about__why ul { display: flex; flex-direction: column; }
.about__why li {
  font-family: var(--f-label); font-weight: 500; font-size: var(--text-xs); color: var(--ink);
  padding: 0.55rem 0; border-bottom: 1px solid var(--line);
  display: flex; gap: 0.9rem; align-items: baseline;
}
@media (max-width: 800px) {
  .about__body { grid-column: 1 / -1; }
  .about__skills { grid-column: 1 / -1; margin-top: 1rem; }
}

/* ============================================================
   FOOTER
   ============================================================ */
.site-footer {
  border-top: 1.5px solid var(--ink); margin-top: clamp(2rem, 6vw, 5rem);
  padding-block: clamp(3rem, 7vw, 6rem);
}
.footer__top { display: grid; grid-template-columns: repeat(12, 1fr); gap: 2rem 1rem; align-items: start; }
.footer__cta { grid-column: 1 / 8; }
.footer__cta .kicker { margin-bottom: 0.9rem; }
.footer__cta .avail:not([hidden]) { display: flex; margin-bottom: 1.4rem; }
.footer__mail {
  font-family: var(--f-display); font-weight: 500;
  font-size: var(--text-footer-mail); letter-spacing: -0.01em; line-height: 1.2;
  display: inline-block; position: relative;
}
.footer__mail::after {
  content: ""; position: absolute; left: 0; bottom: 0.05em; height: 3px; width: 100%;
  background: var(--accent); transform: scaleX(0); transform-origin: left;
  transition: transform var(--dur-hover) var(--ease);
}
@media (hover: hover) and (pointer: fine) {
  .footer__mail:hover::after { transform: scaleX(1); }
}
.footer__right { grid-column: 9 / -1; display: flex; flex-direction: column; gap: 1.6rem; }
.footer__social { display: flex; flex-direction: column; gap: 0.2rem; }
.footer__social[hidden] { display: none; }
.footer__social a {
  font-family: var(--f-label); font-weight: 500; font-size: var(--text-xs); letter-spacing: 0.04em;
  padding: 0.55rem 0; border-bottom: 1px solid var(--line);
  display: flex; justify-content: space-between; align-items: center; color: var(--ink-2);
  transition: color 0.25s var(--ease), padding-left 0.3s var(--ease);
}
.footer__social a .ar { transition: transform var(--dur-hover) var(--ease); }
@media (hover: hover) and (pointer: fine) {
  .footer__social a:hover { color: var(--ink); padding-left: 0.5rem; }
  .footer__social a:hover .ar { transform: translate(3px, -3px); }
}

/* "working with me" — same ruled rows as the socials list, but plain text */
.footer__working h3 {
  font-family: var(--f-label); font-weight: 500; font-size: var(--text-2xs); letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--ink-3); margin-bottom: 0.9rem; padding-bottom: 0.6rem; border-bottom: 1px solid var(--line);
}
.footer__working ul { display: flex; flex-direction: column; }
.footer__working li {
  font-family: var(--f-label); font-weight: 500; font-size: var(--text-xs); letter-spacing: 0.04em; color: var(--ink-2);
  padding: 0.55rem 0; border-bottom: 1px solid var(--line);
}
.footer__bottom {
  margin-top: clamp(2.5rem, 6vw, 4.5rem); display: flex; justify-content: space-between;
  gap: 1rem; flex-wrap: wrap;
  font-family: var(--f-label); font-weight: 500; font-size: var(--text-2xs); color: var(--ink-3); letter-spacing: 0.04em;
}
@media (max-width: 720px) {
  .footer__cta, .footer__right { grid-column: 1 / -1; }
  .footer__right { margin-top: 1rem; }
}

/* ============================================================
   MODAL
   ============================================================ */
.modal {
  position: fixed; inset: 0; z-index: 80;
  display: grid; place-items: center; padding: clamp(0.5rem, 3vw, 2.5rem);
}
.modal[hidden] { display: none; }
.modal__backdrop {
  position: absolute; inset: 0;
  background: color-mix(in srgb, var(--ink) 46%, transparent);
  backdrop-filter: blur(7px); -webkit-backdrop-filter: blur(7px);
  opacity: 0; transition: opacity 0.35s var(--ease);
}
.modal.is-open .modal__backdrop { opacity: 1; }
.modal__panel {
  position: relative; z-index: 1;
  width: min(980px, 100%); max-height: min(90vh, 900px);
  background: var(--paper); border: 1.5px solid var(--ink);
  box-shadow: 22px 22px 0 -2px color-mix(in srgb, var(--accent) 90%, var(--ink));
  display: grid; grid-template-columns: 1.05fr 1fr; overflow: hidden;
  opacity: 0; transform: translateY(26px) scale(0.975);
  transition: opacity 0.5s var(--ease), transform 0.55s var(--ease);
}
.modal.is-open .modal__panel { opacity: 1; transform: none; }
/* the exit is its own, shorter move — not the entrance played backwards and cut
   off by a timer. closeModal() holds is-closing for the 0.25s it takes */
.modal.is-closing .modal__backdrop { transition-duration: var(--dur-ui); }
.modal.is-closing .modal__panel {
  transform: translateY(10px) scale(0.985);
  transition: opacity var(--dur-ui) var(--ease), transform 0.25s var(--ease);
}
/* prev / next: the case fades out, swaps, fades back in (navigate()) instead of
   the old content vanishing in one frame */
.modal__media, .modal__body { transition: opacity 0.18s var(--ease); }
.modal.is-swapping .modal__media, .modal.is-swapping .modal__body { opacity: 0; }
.modal__media {
  background: var(--plate); border-right: 1.5px solid var(--ink);
  padding: clamp(1.5rem, 3vw, 2.5rem);
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 0.9rem;
}
.modal__cover {
  display: block; width: 100%; padding: 1px; background: var(--line-2);
  clip-path: polygon(0 0, calc(100% - var(--cut)) 0, 100% var(--cut), 100% 100%, 0 100%);
}
/* the frame now draws the hairline, so the art must not draw a second one */
.modal__cover .cover-svg { width: 100%; height: auto; }
/* small caption under the modal cover (e.g. abstracted / concept visuals) */
.modal__covernote {
  font-family: var(--f-label); font-weight: 500; font-size: var(--text-3xs); letter-spacing: 0.08em;
  color: var(--ink-3); text-align: center; max-width: 42ch; line-height: 1.5;
}
.modal__covernote[hidden] { display: none; }
.modal__bodywrap {
  position: relative; min-height: 0; overflow: hidden;
  display: flex; flex-direction: column;
  /* bound the scroller by the viewport directly: the panel only has a
     max-height (no definite height), so a grid track alone won't cap it */
  max-height: min(90vh, 900px);
}
.modal__body {
  flex: 1 1 auto; min-height: 0;
  padding: clamp(1.6rem, 3vw, 2.75rem);
  overflow-y: auto; overscroll-behavior: contain; -webkit-overflow-scrolling: touch;
}
/* fade hint that the case body scrolls (toggled by main.js) */
.modal__fade {
  position: absolute; left: 0; right: 0; bottom: 0; height: 2.75rem; z-index: 2;
  pointer-events: none; opacity: 0;
  background: linear-gradient(to top, var(--paper), transparent);
  transition: opacity 0.3s var(--ease);
}
/* seated flush in the panel's top-right corner; the ink seams on its inner
   (left/bottom) edges match the 1.5px frame so it reads as part of the frame */
.modal__close {
  position: absolute; top: 0; right: 0; z-index: 4;
  width: 3rem; height: 3rem; display: grid; place-items: center;
  color: var(--paper); background: var(--ink);
  border-left: 1.5px solid var(--ink); border-bottom: 1.5px solid var(--ink);
  transition: background 0.25s var(--ease), color 0.25s var(--ease);
}
.modal__close svg { width: 1.15rem; height: 1.15rem; }
@media (hover: hover) and (pointer: fine) {
  .modal__close:hover { background: var(--accent); color: var(--paper); }
}
.modal__close:focus-visible { outline: 2px solid var(--paper); outline-offset: -7px; }

.modal__kicker {
  font-family: var(--f-label); font-weight: 500; font-size: var(--text-2xs); letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--accent); margin-bottom: 0.9rem; padding-right: 2.75rem;
}
.modal__title {
  font-family: var(--f-display); font-weight: 500; font-size: var(--text-modal-title);
  letter-spacing: -0.01em; line-height: 1.02;
}
.modal__meta {
  display: flex; align-items: center; flex-wrap: wrap; gap: 0.7rem;
  font-family: var(--f-label); font-weight: 500; font-size: var(--text-2xs); color: var(--ink-2);
  margin-top: 0.9rem; padding-bottom: 1.1rem; border-bottom: 1px solid var(--line);
}
.modal__meta .dot { width: 3px; height: 3px; border-radius: 50%; background: var(--ink-3); }
.badge {
  display: inline-flex; align-items: center; gap: 0.45rem;
  padding: 0.22rem 0.6rem; border-radius: 99px; text-transform: uppercase; letter-spacing: 0.08em;
  font-size: var(--text-3xs); border: 1px solid var(--line-2);
}
.badge__dot { width: 0.45rem; height: 0.45rem; border-radius: 50%; }
.badge--wip { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 45%, transparent); }
.badge--wip .badge__dot { background: var(--accent); }
.badge--shipped { color: var(--ink-2); }
.badge--shipped .badge__dot { background: #1F7A46; }

/* one-line role label under the meta */
.modal__role {
  font-family: var(--f-label); font-weight: 500; font-size: var(--text-2xs); letter-spacing: 0.05em;
  color: var(--ink-3); margin-top: 0.75rem;
}
.modal__role[hidden] { display: none; }

.modal__tagline {
  font-family: var(--f-text); font-size: var(--text-modal-tagline);
  line-height: 1.4; color: var(--ink); margin-top: 1.3rem;
}
/* "key details" — ruled hairline list, same feel as the about skills list */
.modal__facts { display: flex; flex-direction: column; border-top: 1px solid var(--line); }
.modal__facts li {
  font-family: var(--f-text); font-size: var(--text-sm); color: var(--ink-2);
  padding: 0.55rem 0; border-bottom: 1px solid var(--line);
}
#modal-facts-sec[hidden] { display: none; }
.modal__section { margin-top: 1.6rem; }
.modal__section h3 {
  font-family: var(--f-label); font-weight: 500; font-size: var(--text-3xs); letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--ink-3); margin-bottom: 0.55rem;
}
.modal__section p { color: var(--ink-2); font-size: var(--text-sm); }
.chips { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.chip {
  font-family: var(--f-label); font-weight: 500; font-size: var(--text-2xs); color: var(--ink);
  padding: 0.4rem 0.75rem; border: 1px solid var(--line-2); border-radius: 99px;
  background: var(--plate);
}

/* real product screenshots as covers */
.cover-img { object-fit: cover; object-position: center top; background: var(--plate); }
.cover-img--contain { object-fit: contain; padding: clamp(0.9rem, 2vw, 1.5rem); }
.modal__cover .cover-img { height: auto; max-height: min(58vh, 540px); object-fit: contain; }

/* case-panel footer: prev/next on the left, action buttons on the right */
.modal__footer {
  margin-top: 1.9rem; padding-top: 1.4rem; border-top: 1px solid var(--line);
  display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 1rem;
}
.modal__nav { display: inline-flex; gap: 0.4rem; }
.modal__navbtn {
  font-family: var(--f-label); font-weight: 500; font-size: var(--text-3xs); letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--ink-2); background: transparent; border: 1px solid var(--line-2); padding: 0.55rem 0.75rem;
  display: inline-flex; align-items: center; gap: 0.4rem;
  transition: color 0.25s var(--ease), border-color 0.25s var(--ease);
}
.modal__navbtn .ar { transition: transform var(--dur-hover) var(--ease); }
.modal__navbtn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.modal__actions { display: inline-flex; flex-wrap: wrap; align-items: stretch; gap: 0.6rem; }

/* shared metrics so VIEW LIVE (ink) + START A PROJECT (ink outline) match heights */
.modal__live, .modal__cta {
  display: inline-flex; align-items: center; gap: 0.55rem;
  font-family: var(--f-label); font-weight: 500; font-size: var(--text-2xs);
  letter-spacing: 0.16em; text-transform: uppercase; padding: 0.8rem 1.35rem;
  border: 1px solid var(--ink);
}
.modal__live { color: var(--paper); background: var(--ink); transition: background 0.25s var(--ease); }
.modal__cta { color: var(--ink); background: transparent; transition: background 0.25s var(--ease), color 0.25s var(--ease); }
.modal__live .ar, .modal__cta .ar { transition: transform var(--dur-hover) var(--ease); }
@media (hover: hover) and (pointer: fine) {
  .modal__live:hover { background: var(--accent); border-color: var(--accent); }
  .modal__live:hover .ar { transform: translate(2px, -2px); }
  .modal__cta:hover { background: var(--ink); color: var(--paper); }
  .modal__cta:hover .ar { transform: translate(2px, -2px); }
  .modal__navbtn:hover { color: var(--ink); border-color: var(--ink); }
}

@media (max-width: 760px) {
  .modal { padding: 0; place-items: end stretch; }
  .modal__panel {
    grid-template-columns: 1fr; max-height: 100dvh; height: 100dvh; width: 100%;
    border: 0; box-shadow: none; transform: translateY(100%);
    grid-template-rows: auto 1fr;
  }
  .modal.is-open .modal__panel { transform: none; }
  .modal__media { border-right: 0; border-bottom: 1.5px solid var(--ink); padding: 1.5rem 1.5rem 1.25rem; }
  .modal__cover { max-width: 420px; margin-inline: auto; }
  .modal__body { padding-bottom: 3rem; }
  /* mobile uses the definite 100dvh panel + `auto 1fr` rows to bound the body */
  .modal__bodywrap { max-height: none; }
}

body.is-locked { overflow: hidden; }

/* ============================================================
   REVEAL (progressive-enhancement; hidden only when JS present)
   ============================================================ */
html.js .reveal {
  opacity: 0; transform: translateY(18px);
  transition: opacity 0.85s var(--ease), transform 0.85s var(--ease);
  transition-delay: calc(var(--i, 0) * 75ms);
  will-change: opacity, transform;
}
html.js .reveal.is-in { opacity: 1; transform: none; }

/* About's body reads line by line. Each word gets its own clip box and every
   word on the same visual line carries the same delay, which looks identical
   to masking whole lines — and unlike a line wrapper it cannot tear the
   inline <span class="em"> in the lead paragraph in half.
   clip-path rather than overflow: overflow on an inline-block moves its
   baseline to the bottom margin edge, which would shift the text. */
/* A hyphenated word breaks in plain text but not once it is an inline-block,
   so the paragraph would re-wrap the moment the split is undone and shift the
   words sideways. Keeping it unbreakable in both states removes the jump. */
.nobr { white-space: nowrap; }

/* the 2px of slack at the bottom is for descenders: this face overflows its
   line box, so a tight box clips the tails off g, p and y. The word waiting
   below sits further down than that, so nothing peeks through. */
.w { display: inline-block; clip-path: inset(0 0 -2px 0); }
html.js .reveal--lines .w__in {
  display: inline-block;
  /* the 3px covers the 2px of descender slack in .w whatever the size: at
     small sizes 10% of a line is barely more than the slack, and the waiting
     word would show its top edge through it */
  transform: translateY(calc(110% + 3px));
  transition: transform 0.75s var(--ease);
  transition-delay: calc(var(--ln, 0) * 90ms);
}
html.js .reveal--lines.is-in .w__in { transform: none; }
/* the words carry the motion, so the block itself must not move. will-change
   from .reveal would hold a compositing layer this block never animates. */
html.js .reveal--lines { opacity: 1; transform: none; transition: none; will-change: auto; }

/* The highlighter sweep is painted on the emphasis element, not on the words,
   so while the words are still below their clip boxes it would hang in the air
   as an empty bar. Collapse it while the split is live and grow it in on the
   beat of its own line. */
html.js .reveal--lines.is-splitting .em {
  background-repeat: no-repeat;
  background-size: 100% 0;
}
html.js .reveal--lines.is-splitting.is-in .em {
  background-size: 100% 100%;
  transition: background-size 0.55s var(--ease);
  transition-delay: calc(var(--ln, 0) * 90ms + 120ms);
}

/* ============================================================
   PLAYFUL DETAILS — the motion layer
   ============================================================ */

/* scroll progress hairline */
.progress {
  position: fixed; top: 0; left: 0; right: 0; height: 2px; z-index: 90;
  background: var(--ikb); transform-origin: left; transform: scaleX(0);
  pointer-events: none;
}

/* trailing cursor dot (created only on hover+fine pointers) */
.cursor-dot {
  position: fixed; top: 0; left: 0; width: 9px; height: 9px; border-radius: 50%;
  background: var(--ikb); opacity: 0.4; z-index: 95; pointer-events: none;
}

/* covers tilt toward the cursor (transform driven by main.js) */
.plate__frame { transition: transform 0.35s var(--ease); }
@media (hover: hover) and (pointer: fine) {
  .plate__frame:hover { transition: transform 0.08s linear; }
}

/* hero name — per-letter staggered entrance; the block reveal steps aside */
.hero__name .word { display: inline-block; white-space: nowrap; }
.hero__name .ch { display: inline-block; }
html.js .hero__name.reveal { opacity: 1; transform: none; transition: none; }
html.js .hero__name .ch {
  transform: translateY(112%); opacity: 0;
  transition: transform 0.75s var(--ease), opacity 0.45s ease;
  transition-delay: calc(var(--ci) * 42ms + 120ms);
}
html.js .hero__name.is-in .ch { transform: none; opacity: 1; }
/* once every letter has landed (main.js settleHeroName) the letters take the hover
   timing for the lift and its return. The old hover rule lost to the html.js
   entrance rule on specificity, so the wave never played (found 2026-09-02) */
html.js .hero__name.is-settled .ch { transition: transform 0.28s var(--ease); transition-delay: calc(var(--ci) * 16ms); }
@media (hover: hover) and (pointer: fine) {
  html.js .hero__name.is-settled:hover .ch { transform: translateY(-0.07em); }
}

/* modal content — staggered rise each time a case opens */
@keyframes rise-in {
  from { opacity: 0; transform: translateY(14px); }
  to { opacity: 1; transform: none; }
}
@keyframes cover-in {
  from { opacity: 0; transform: scale(1.035); }
  to { opacity: 1; transform: none; }
}
.modal.is-open .modal__cover { animation: cover-in 0.55s var(--ease) 0.08s backwards; }
.modal.is-open .modal__body > * { animation: rise-in 0.4s var(--ease) backwards; }
/* 35ms steps: the whole case has landed in ~0.8s instead of ~1.1s (2026-09-02 audit) */
.modal.is-open .modal__body > *:nth-child(1) { animation-delay: 0.08s; }
.modal.is-open .modal__body > *:nth-child(2) { animation-delay: 0.115s; }
.modal.is-open .modal__body > *:nth-child(3) { animation-delay: 0.15s; }
.modal.is-open .modal__body > *:nth-child(4) { animation-delay: 0.185s; }
.modal.is-open .modal__body > *:nth-child(5) { animation-delay: 0.22s; }
.modal.is-open .modal__body > *:nth-child(6) { animation-delay: 0.255s; }
.modal.is-open .modal__body > *:nth-child(7) { animation-delay: 0.29s; }
.modal.is-open .modal__body > *:nth-child(8) { animation-delay: 0.325s; }
.modal.is-open .modal__body > *:nth-child(9) { animation-delay: 0.36s; }
.modal.is-open .modal__body > *:nth-child(10) { animation-delay: 0.395s; }

/* soft pulse — availability dot + in-development status dots */
/* the pulse is a ring on ::after moving only transform and opacity (compositor
   work). Animating box-shadow repainted the dot every frame, for ever */
@keyframes pulse-ring {
  from { transform: scale(1); opacity: 0.38; }
  70%, to { transform: scale(3); opacity: 0; }
}
.avail__dot, .plate__status-dot { position: relative; }
.avail__dot::after, .plate__status-dot::after {
  content: ""; position: absolute; inset: 0; border-radius: 50%; background: currentColor;
  animation: pulse-ring 2.4s ease-out infinite;
}
.avail__dot { color: #1F7A46; }
.plate__status-dot { color: var(--ikb); }

/* section rules draw themselves in */
.sec-head { border-top: 0; position: relative; }
.sec-head::before {
  content: ""; position: absolute; top: 0; left: 0; width: 100%; height: 1.5px;
  background: var(--ink); transform: scaleX(0); transform-origin: left;
  transition: transform 1s var(--ease) 0.1s;
}
.sec-head.is-in::before, html:not(.js) .sec-head::before { transform: scaleX(1); }

/* covers unveil with an editorial curtain wipe (top → bottom); both states
   are the same polygon (collapsed to 0 height, then full) so the cut corner
   survives the wipe instead of the shape jumping to inset()'s square corner */
html.js .plate.reveal .plate__cover {
  clip-path: polygon(0 0, calc(100% - var(--cut)) 0, 100% 0, 100% 0, 0 0);
  transition: clip-path 0.9s var(--ease);
  transition-delay: calc(var(--i, 0) * 90ms + 60ms);
}
html.js .plate.reveal.is-in .plate__cover {
  clip-path: polygon(0 0, calc(100% - var(--cut)) 0, 100% var(--cut), 100% 100%, 0 100%);
}

/* section titles slide in from the left as their rule draws */
html.js .sec-head.reveal .sec-head__title {
  opacity: 0; transform: translateX(-14px);
  transition: opacity 0.7s var(--ease), transform 0.7s var(--ease);
}
html.js .sec-head.reveal.is-in .sec-head__title { opacity: 1; transform: none; }

/* caption lifts a touch with the cover on hover */
@media (hover: hover) and (pointer: fine) {
  .plate__title { transition: transform var(--dur-hover) var(--ease); }
  .plate__btn:hover .plate__title { transform: translateY(-2px); }
}

/* ruled lists nudge on hover */
@media (hover: hover) and (pointer: fine) {
  .about__skills li, .about__why li, #footer-working li { transition: padding-left 0.25s var(--ease); }
  .about__skills li:hover, .about__why li:hover, #footer-working li:hover { padding-left: 0.45rem; }
}

@media (prefers-reduced-motion: reduce) {
  /* "less and calmer", not zero (2026-09-02 audit): every movement is removed,
     but the modal's opacity fade and colour changes stay, so nothing is abrupt */
  html { scroll-behavior: auto !important; }
  html.js .reveal, html.js .sec-head.reveal .sec-head__title { opacity: 1; transform: none; transition: none; }
  html.js .hero__name .ch, html.js .hero__name.is-settled .ch, html.js .hero__name.is-settled:hover .ch { transform: none; opacity: 1; transition: none; }
  html.js .reveal--lines .w__in { transform: none; transition: none; }
  html.js .reveal--lines.is-splitting .em, html.js .reveal--lines.is-splitting.is-in .em { background-size: 100% 100%; transition: none; }
  html.js .plate.reveal .plate__cover {
    clip-path: polygon(0 0, calc(100% - var(--cut)) 0, 100% var(--cut), 100% 100%, 0 100%);
    transition: none;
  }
  .sec-head::before { transform: scaleX(1); transition: none; }
  .modal__backdrop, .modal.is-closing .modal__backdrop { transition: opacity 0.2s var(--ease); }
  .modal__panel, .modal.is-closing .modal__panel { transform: none; transition: opacity 0.2s var(--ease); }
  .modal.is-open .modal__cover, .modal.is-open .modal__body > * { animation: none; }
  .modal__media, .modal__body { transition: none; }
  .cursor-dot { display: none; }
  .avail__dot::after, .plate__status-dot::after { display: none; }
}

/* ---------- print ----------
   Browsers drop backgrounds by default, which would leave the two dark panels
   and the board printing light-on-white. Measured from a PDF with backgrounds
   off: Chrome darkens light text to compensate, but only so far — the panel
   heads land at 2.6:1 (ink) and 2.3:1 (vermilion), and the board's pure white
   is not darkened at all, so it prints at 1.0:1 and vanishes. A browser that
   does not compensate loses the heads too. So on paper nothing is a dark
   surface: the panels take the root tokens back — the same values, so all three
   end up one colour and the mat stays brighter than them — and the decorative
   board is dropped entirely. */
@media print {
  .ticker { display: none; }
  .disc:nth-of-type(3n+2),
  .disc:nth-of-type(3n) {
    --paper:  #F6F6F6;
    --ink:    #17150E;
    --ink-2:  #4C483B;
    --ink-3:  #6B6656;
    --line:   rgba(23, 21, 14, 0.14);
    --line-2: rgba(23, 21, 14, 0.28);
    --ikb:    #122AD6;
    /* the top rule is hidden on screen because the surface change separates the
       panels; on paper all three are white, so it is the only thing that does */
    border-top-color: var(--ink);
  }
}
