
/* Values match the Figma "Lab" page (Lab / XL). Monochrome shell; colour only
   comes from the work and imagery. */
:root {
  --bg: #fff;
  --ink: #000;
  --ink-inverse: #fff;
  --muted: #5e5e5e;
  --line: #e6e6e6;
  --surface: #fafafa;
  --shadow: rgb(0 0 0 / 0.12);
  --serif: "Instrument Serif", "Times New Roman", serif;
  --sans: "IBM Plex Sans", "Helvetica Neue", Arial, sans-serif;
  --mono: "IBM Plex Mono", ui-monospace, monospace;
  --gutter: 20px; /* the page edge, as on shahvirsarkary.com: 20px, 16px on phones */
  --ease: cubic-bezier(.2, .7, .2, 1);
  color-scheme: light;
}
/* data-theme is set before paint: the saved choice, or the device setting */
:root[data-theme="dark"] {
  --bg: #0a0a0a;
  --ink: #fff;
  --ink-inverse: #000;
  --muted: #a3a3a3;
  --line: #262626;
  --surface: #141414; /* a touch lighter than #0f0f0f, which all but vanished on #0a0a0a */
  --shadow: rgb(255 255 255 / 0.2);
  color-scheme: dark;
}
[data-theme="dark"] .on-light, :root:not([data-theme="dark"]) .on-dark { display: none; }
/* Selected text: black in light mode, white in dark mode */
::selection { background: var(--ink); color: var(--ink-inverse); }

*, *::before, *::after { box-sizing: border-box; }
html { scroll-behavior: smooth; scroll-padding-top: 72px; }
body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font: 400 14px/20px var(--sans);
  -webkit-font-smoothing: antialiased;
  overflow-x: clip;
}
img { display: block; max-width: 100%; height: auto; }
button { font: inherit; }
a { color: inherit; }
:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; border-radius: 4px; }
ul, ol, dl, dd, figure, blockquote, p, h1, h2, h3 { margin: 0; padding: 0; }
ul, ol { list-style: none; }
h1, h2, h3 { font-weight: 400; }

.visually-hidden {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}
/* Skip link and a pause button, both shown together when either has focus */
.skips { position: fixed; top: 8px; left: 8px; z-index: 20; display: flex; gap: 8px; pointer-events: none; }
.skips:focus-within { pointer-events: auto; } /* hidden, it would sit over the logo and catch its clicks */
.skip {
  padding: 8px 14px; border: 0; background: var(--ink); color: var(--bg);
  font: inherit; border-radius: 999px; text-decoration: none; cursor: pointer;
  transform: translateY(-200%);
}
.skips:focus-within .skip { transform: none; }

.label {
  font: 500 12px/20px var(--mono);
  letter-spacing: .08em;
  color: var(--muted);
}
/* Labels that type in on scroll (site.js typeLabels) hold their room until each key lands */
.label.is-typing .key:not(.is-typed, .is-ghost) { visibility: hidden; }
/* The suggested rest of a word: faint until it's accepted, then it settles to the label's colour */
.label .key.is-ghost { opacity: .35; }
.label.is-typing .key { transition: opacity .16s ease; }
.label .caret::after { top: -11px; height: 13px; }
/* While a suggestion is on offer the caret idles, blinking, as an editor's does */
.label .caret.is-idle::after { animation: caret-blink 1.06s steps(1) infinite; }

/* Top bar */
.top {
  position: fixed; inset: 0 0 auto; z-index: 10;
  display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 12px;
  padding: 12px var(--gutter);
  font-size: 12px; line-height: 24px;
  background: color-mix(in srgb, var(--bg) 90%, transparent);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
}
.top a { text-decoration: none; }
.top-home { justify-self: start; display: block; width: 32px; height: 32px; border-radius: 50%; }
.logo { display: block; width: 32px; height: 32px; }
.logo-bg { fill: var(--bg); }
.logo-s { fill: var(--ink); }
.top-links { display: flex; gap: 20px; }
.top-end { justify-self: end; display: flex; align-items: center; gap: 20px; }
/* Menu (phones only): set like the desktop nav links (Plex Sans 12/24); its + turns to × when open */
.menu-btn {
  display: none; align-items: center; gap: 6px;
  padding: 4px 0; border: 0; background: none; cursor: pointer;
  font: 400 12px/24px var(--sans); color: var(--ink);
}
.menu-btn__icon { width: 13px; height: 13px; transition: rotate .4s cubic-bezier(.5, 0, 0, 1); }
.menu-btn[aria-expanded="true"] .menu-btn__icon { rotate: 45deg; }
.menu {
  position: fixed; inset: 0; z-index: 9; /* under the top bar, which keeps the logo and Close */
  display: flex; flex-direction: column; justify-content: space-between;
  padding: 104px var(--gutter) 40px;
  background: var(--bg);
  overflow-y: auto; overscroll-behavior: contain;
}
.menu[hidden] { display: none; }
.menu ul { margin: 0; padding: 0; list-style: none; }
.menu .menu-links { padding-top: 16px; } /* beats the reset above */
.menu a { color: var(--ink); text-decoration: none; }
.menu-sections { display: grid; gap: 4px; }
.menu-sections a { display: block; width: fit-content; font: 400 72px/80px var(--serif); letter-spacing: -.03em; }
.menu-sections a:focus-visible { font-style: italic; }
@media (hover: hover) { .menu-sections a:hover { font-style: italic; } }
.menu-links {
  display: flex; flex-wrap: wrap; gap: 8px 20px; border-top: 1px solid var(--line);
  font-size: 12px; line-height: 24px; /* the desktop nav's links */
}
html.menu-open { overflow: hidden; }
.icon-btn {
  display: grid; place-items: center;
  width: 32px; height: 32px; padding: 0;
  border: 0; background: none; color: inherit; cursor: pointer; border-radius: 50%;
}
@media (hover: hover) { .icon-btn:hover { color: var(--ink); } }
.theme-icon {
  width: 14px; height: 14px; border-radius: 50%;
  border: 1.5px solid currentColor;
  background: linear-gradient(90deg, currentColor 50%, transparent 50%);
  transition: rotate .4s var(--ease);
}
[data-theme="dark"] .theme-icon { rotate: 180deg; }
/* Same 14px circle as the theme icon: pause bars while playing, a play triangle when paused */
.motion-icon { width: 14px; height: 14px; overflow: visible; fill: none; stroke: currentColor; stroke-width: 1.5; }
.motion-icon__play { fill: currentColor; stroke: none; display: none; }
[data-motion="paused"] .motion-icon__pause { display: none; }
[data-motion="paused"] .motion-icon__play { display: inline; }

/* Hero */
/* The name's foot always sits --bleed below the fold, so a bit of it is cut off by the screen edge.
   --name-size comes from fitName() in app.js. Before it runs, the name is sized from the page
   width by its own width-to-size ratio (--name-ratio), so the fit doesn't jolt the page. */
.hero {
  --name-ratio: 4.1; /* "Stefan Adams" in Instrument Serif is 4.1 times as wide as its size */
  --bleed: calc(var(--name-size, (100vw - var(--gutter)) / var(--name-ratio)) * .1);
  min-height: calc(100svh + var(--bleed) + 112px);
  display: flex; flex-direction: column; align-items: center; justify-content: flex-end;
  padding: 120px 0 112px;
  text-align: center;
}
.hero-doodle svg { display: block; width: 100%; height: 100%; overflow: visible; }
.hero-intro {
  width: min(344px, calc(100% - 2 * var(--gutter)));
  padding: 16px 0 81px;
  box-sizing: content-box;
  font-size: 16px; line-height: 24px;
  color: var(--muted);
}
.hero-intro s { text-decoration-thickness: 1px; }
/* The edit (app.js): "users" is selected like text, struck through, let go, then " people."
   is typed after it. Untyped letters take no room, so the centred line grows from its
   middle as each one lands, as typing in centre-aligned text does. Screen readers get
   the whole sentence from a hidden copy meanwhile. Without JavaScript, with Reduce
   motion or with animations paused, it's simply the finished sentence. */
.hero-intro.is-editing .edit-old {
  text-decoration-color: transparent;
  background: linear-gradient(var(--ink), var(--ink)) left / 0 100% no-repeat;
  transition: background-size .35s var(--ease), color .2s var(--ease);
}
.hero-intro.is-editing .edit-old.is-selected:not(.is-let-go) { background-size: 100% 100%; color: var(--ink-inverse); }
.hero-intro.is-editing .edit-old.is-struck { text-decoration-color: currentColor; }
.hero-intro.is-editing .edit-old.is-let-go { background-image: none; }
.hero-intro.is-editing .edit-new .key:not(.is-typed) { display: none; }
/* The caret takes no room, so typing never moves the words around it */
.caret { position: relative; display: inline-block; width: 0; }
.caret::after {
  content: ""; position: absolute; left: 0; top: -15px;
  width: 1px; height: 19px; background: var(--ink);
}
.caret.is-blinking::after { animation: caret-blink 1s steps(1) 2; }
@keyframes caret-blink { 50% { opacity: 0; } }
.hero-name {
  width: 100%;
  padding: 0 calc(var(--gutter) / 2);
  font: 400 calc((100vw - var(--gutter)) / var(--name-ratio))/.79 var(--serif);
  letter-spacing: -.045em;
  white-space: nowrap;
}
.hero-name .fit { display: inline-block; }
.hero-intro, .hero-name { opacity: 0; translate: 0 16px; }
.is-ready .hero-intro { opacity: 1; translate: 0; transition: opacity .8s .1s var(--ease), translate .8s .1s var(--ease); }
.is-ready .hero-name { opacity: 1; translate: 0; transition: opacity 1s .2s var(--ease), translate 1s .2s var(--ease); }

/* 404 (Figma "Lab / 404 XL"): the spectators, a label, the line and a way home, stacked from
   the top, then a huge 404 fitted to the page width like the name. The 404's tight
   tracking also follows its last digit, so the measured word is .045em short of the ink;
   the padding gives it back, or the last 4 runs off the edge. The spectators and text fade
   up as one, like the intro. */
.hero--404 { justify-content: flex-start; min-height: 0; padding-top: 120px; }
/* At this size the digits' line boxes reach well above the heading, over the button, so
   the 404 lets clicks through */
.hero--404 { --name-ratio: 1.156; }
.hero--404 .hero-name { margin-top: 64px; pointer-events: none; }
.hero--404 .fit { padding-right: .045em; }
.nf-art { width: min(160px, calc(100% - 2 * var(--gutter))); margin-top: 40px; }
.nf-text { display: flex; flex-direction: column; align-items: center; gap: 16px; padding-top: 32px; }
.nf-content { display: flex; flex-direction: column; align-items: center; gap: 24px; }
.hero--404 .hero-intro { width: min(344px, 100vw - 2 * var(--gutter)); padding: 0; opacity: 1; translate: 0; }
.secondary-btn { text-decoration: none; }
/* A little terminal looking for the page (404.js types it out). Plex Mono like the career
   dates (Figma Type/label/meta), commands in ink after a muted "$", replies muted. It
   holds its full size from the start, so typing never moves the button. Long addresses
   end in "…". Without JavaScript, with Reduce motion or paused, it's simply all there. */
.nf-log {
  width: fit-content; max-width: min(344px, 100vw - 2 * var(--gutter));
  text-align: left;
  font: 500 12px/20px var(--mono); letter-spacing: .04em; color: var(--muted);
}
.nf-log p { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.nf-log__cmd { color: var(--ink); }
.nf-log__cmd::before { content: "$ "; color: var(--muted); }
.nf-log.is-typing :is(.key:not(.is-typed), p:not(.is-shown)) { visibility: hidden; }
.nf-log .caret::after { top: -11px; height: 13px; }
.nf-art, .nf-text { opacity: 0; translate: 0 16px; }
.is-ready :is(.nf-art, .nf-text) { opacity: 1; translate: 0; transition: opacity .8s .1s var(--ease), translate .8s .1s var(--ease); }

/* Spectators, from the old homepage (Figma "Image / Spectators"): ink lines, the drawings'
   shade. Idle, each sways, breathes or bobs from their feet, the "!" wobbles, pupils
   glance and the moustache twitches. Every few seconds (404.js .is-reacting) they spot
   something: the surprised one hops twice, the "!" pops, the tall man's brow shoots up as
   he leans back, and the others look over. Still with Reduce motion or paused animations. */
.spectators { display: block; width: 100%; height: auto; aspect-ratio: 262 / 270; overflow: visible; }
.spectators .line, .spectators use { fill: var(--ink); }
.spectators .shade { fill: var(--shadow); }
.spectators__tall, .spectators__thinker, .spectators__surprised, .spectators__exclaim { transform-box: fill-box; transform-origin: 50% 100%; }
.spectators__moustache { transform-box: view-box; transform-origin: 39px 86px; }
@media (prefers-reduced-motion: no-preference) {
  .spectators__tall { animation: spectators-sway 4.6s ease-in-out -1s infinite; }
  .spectators__thinker { animation: spectators-breathe 3.3s ease-in-out -.8s infinite; }
  .spectators__surprised { animation: spectators-bob 2.6s ease-in-out infinite; }
  .spectators__exclaim { animation: spectators-wobble 1.8s ease-in-out infinite; }
  .spectators__tall .spectators__glance { animation: spectators-glance-a 4s ease-in-out -1s infinite; }
  .spectators__thinker .spectators__glance { animation: spectators-glance-b 3.4s ease-in-out -2s infinite; }
  .spectators__moustache { animation: spectators-twitch 4.5s ease-in-out -2s infinite; }
  .spectators__look, .spectators__brow, .spectators__tall, .spectators__thinker {
    transition: translate .35s cubic-bezier(.34, 1.56, .64, 1), rotate .5s cubic-bezier(.34, 1.56, .64, 1);
  }
  .spectators.is-reacting .spectators__surprised { animation: spectators-bob 2.6s ease-in-out infinite, spectators-hop 1.1s ease-in-out; }
  .spectators.is-reacting .spectators__exclaim { animation: spectators-wobble 1.8s ease-in-out infinite, spectators-pop .7s ease-out .15s; }
  .spectators.is-reacting .spectators__tall .spectators__look { translate: 2.2px -.8px; transition-delay: .2s; }
  .spectators.is-reacting .spectators__thinker .spectators__look { translate: 1.2px -.6px; transition-delay: .3s; }
  .spectators.is-reacting .spectators__tall .spectators__brow { translate: 0 -3.5px; transition-delay: .2s; }
  .spectators.is-reacting .spectators__tall { rotate: -3deg; transition-delay: .25s; }
  .spectators.is-reacting .spectators__thinker { rotate: 6deg; transition-delay: .35s; }
}
[data-motion="paused"] :is(.spectators, .spectators *) { animation: none !important; transition: none !important; }
@keyframes spectators-sway { 0%, 100% { transform: rotate(-1.5deg); } 50% { transform: rotate(1.5deg); } }
@keyframes spectators-breathe { 0%, 100% { transform: scale(1, 1); } 50% { transform: scale(1.02, 1.04); } }
@keyframes spectators-bob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-2px); } }
@keyframes spectators-wobble { 0%, 100% { transform: rotate(-6deg); } 50% { transform: rotate(6deg); } }
@keyframes spectators-glance-a { 0%, 8%, 38%, 46%, 92%, 100% { translate: 0 0; } 16%, 30% { translate: -2.2px .3px; } 54%, 66% { translate: 2px -.8px; } 74%, 84% { translate: -.8px -1px; } }
@keyframes spectators-glance-b { 0%, 6%, 36%, 44%, 90%, 100% { translate: 0 0; } 14%, 28% { translate: 1.2px .3px; } 52%, 64% { translate: -1.1px -.5px; } 72%, 82% { translate: .6px -.8px; } }
@keyframes spectators-twitch { 0%, 84%, 96%, 100% { transform: none; } 86% { transform: scale(1.14, .84) rotate(-6deg); } 88.5% { transform: scale(.94, 1.1) rotate(5deg); } 91% { transform: scale(1.1, .9) rotate(-4deg); } 93.5% { transform: scale(.98, 1.03) rotate(2deg); } }
@keyframes spectators-hop { 0%, 100% { translate: 0 0; scale: 1 1; } 10% { translate: 0 0; scale: 1.06 .92; } 25% { translate: 0 -14px; scale: .95 1.06; } 42% { translate: 0 0; scale: 1.07 .92; } 60% { translate: 0 -9px; scale: .97 1.04; } 78% { translate: 0 0; scale: 1.04 .96; } }
@keyframes spectators-pop { 0%, 100% { scale: 1; rotate: 0deg; translate: 0 0; } 20% { scale: .6; translate: 0 2px; } 55% { scale: 1.6; rotate: 14deg; translate: 0 -6px; } 80% { scale: .9; rotate: -6deg; translate: 0 0; } }

/* Work */
.work { display: grid; gap: 48px; padding: 0 var(--gutter) 140px; }
.work-list { display: grid; gap: 96px; }
.work-link { display: block; text-decoration: none; }
/* The image boxes keep the Figma frame's 1360 × 635 shape at every width. Inside, the live
   site's XL tile (560px-tall art plus its animated parts, which are placed in the art's own
   pixels) sits on a stage that app.js scales to the box's height (--tile-scale) */
.work-art {
  position: relative; display: block; overflow: hidden;
  aspect-ratio: 1360 / 635;
  border: 1px solid var(--line); border-radius: 16px;
  transition: translate .4s var(--ease);
}
/* The transparent 1px bottom border stands in for the live card's border: the art is drawn
   from below it and the animated parts are placed above it, exactly as on the live card */
.work-stage {
  position: absolute; left: 50%; bottom: -1px;
  width: var(--stage-w); height: 560px;
  border-bottom: 1px solid transparent;
  box-sizing: content-box;
  background: var(--tile-light) center bottom / auto 560px no-repeat border-box;
  translate: -50% 0;
  scale: var(--tile-scale, 1);
  transform-origin: 50% 100%;
}
[data-theme="dark"] .work-stage { background-image: var(--tile-dark); }
@media (hover: hover) { .work-link:hover .work-art { translate: 0 -6px; } }
/* Figma "Lab / Work meta": above each image, under a line, columns set per size (XL / L / M) */
.work-meta {
  display: grid; grid-template-columns: 174px 349px 1fr auto; column-gap: 24px; row-gap: 8px;
  align-items: start;
  padding: 24px 0;
  border-top: 1px solid var(--line);
}
.work-tag { font: 500 12px/20px var(--mono); letter-spacing: .08em; color: var(--muted); }
.work-title { justify-self: start; font-weight: 500; } /* only as wide as its words, for the hover highlight */

.work-desc { color: var(--muted); max-width: 470px; }
/* The stat: a big serif number with a small mono line under it saying what it counts */
.work-stat { display: grid; justify-items: end; gap: 6px; text-align: right; }
.work-stat__num { font: 400 40px/40px var(--serif); letter-spacing: -.02em; }
.work-stat__label { font: 500 12px/20px var(--mono); letter-spacing: .04em; color: var(--muted); } /* Type/label/meta */
@media (max-width: 1279px) {
  .work-meta { grid-template-columns: 118px 236px 1fr auto; }
  .work-stat__num { font-size: 32px; line-height: 32px; }
}
@media (max-width: 1023px) {
  .work-meta { grid-template-columns: 110px 160px 1fr auto; }
  .work-stat__num { font-size: 28px; line-height: 28px; }
}

/* Statement */
.statement {
  display: flex; flex-direction: column; align-items: center; gap: 32px;
  padding: 140px var(--gutter);
  text-align: center;
  border-top: 1px solid var(--line);
}
.statement-heading { display: flex; flex-direction: column; align-items: center; gap: 16px; }
.statement-text {
  max-width: 720px;
  font: 400 clamp(44px, 6.4vw, 92px)/.978 var(--serif);
  text-wrap: balance;
  letter-spacing: -.03em;
}
/* Every pill is 38px, the Figma "Lab" pill component's height in both states, so picking one never changes the sizes */
.pills { display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; }
.pill {
  height: 38px; padding: 0 16px;
  border: 1px solid var(--ink); border-radius: 999px;
  background: none; color: var(--ink); cursor: pointer;
  font: 400 12px/18px var(--sans); /* Type/body/xs, like the chips */
  transition: background-color .2s var(--ease), color .2s var(--ease), scale .15s var(--ease);
}
.pill { display: inline-flex; align-items: center; gap: 8px; }
.pill[aria-selected="true"] { background: var(--ink); color: var(--ink-inverse); }
/* The icon grows in as its pill is picked and shrinks away as it's left, the pill
   widening and narrowing with it (the -8px cancels the flex gap while it's away) */
.pill .skill-icon {
  transition: width .4s var(--ease), margin .4s var(--ease), opacity .3s var(--ease), scale .4s var(--ease);
}
.pill[aria-selected="false"] .skill-icon { width: 0; margin-right: -8px; opacity: 0; scale: .4; }
/* Mobile: always two rows of two, whichever pill is picked. An empty full-width item
   after the second pill forces the break (row gap moved onto the second row's pills). */
@media (max-width: 767px) {
  .pills { row-gap: 0; }
  .pills::after { content: ""; flex-basis: 100%; order: 1; }
  .pill:nth-child(n + 3) { order: 2; margin-top: 8px; }
}
/* The narrowest phones: a little less padding, so a picked pill and its neighbour still fit */
@media (max-width: 359px) { .pill { padding: 0 12px; } }

/* ---------- Skill icons ----------
   Inline SVGs (the hand-drawn Figma icons split into parts) so the parts can move.
   Copied from the live site (css/styles.css). Only the selected pill shows its icon (Figma
   "Lab" pill, Selected=True), and it loops for as long as that pill is selected:
   - Design Systems: the diamond splits into four small diamonds that tile the same
     shape, then they click back into one
   - UX Design: the circle, triangle and square swap places round the corner, one step
     at a time, until each is home again
   - Accessibility: the figure's arms go up, down by its sides, then up for a wave, while
     the ring turns once round
   - Emerging Tech: the big sparkle flares twice, turning, while the small ones blink
     and spin, twice each
   With Reduce motion on, or animations paused, the icons are simply still. */
.skill-icon {
  flex: none;
  width: 20px;
  height: 20px;
  overflow: visible; /* parts travel a little outside the 20×20 drawing */
}

.skill-icon__shadow {
  fill-opacity: 0.12; /* 4 points up on the Figma icons' 8%, in both themes */
}

.skill-icon * {
  transform-box: view-box; /* transform-origin below is in the drawing's 20×20 units */
}

/* The three extra diamonds only show while they're apart */
.skill-icon__tile:not(.skill-icon__tile--1) {
  opacity: 0;
}

@media (prefers-reduced-motion: no-preference) {
  .pill[aria-selected="true"] .skill-icon * {
    animation-duration: var(--skill-icon-time);
    animation-fill-mode: both;
    animation-iteration-count: infinite;
    animation-timing-function: cubic-bezier(0.45, 0, 0.55, 1); /* a soft ease, so no step stalls at its ends */
  }

  /* All four play together and take the same time, so they start and end as one */
  .skill-icon { --skill-icon-time: 2s; }

  /* Design Systems: four half-size diamonds exactly fill the big one */
  .skill-icon__tile { transform-origin: 10px 10px; }
  .pill[aria-selected="true"] .skill-icon .skill-icon__tile--1 { animation-name: skill-tile-1; }
  .pill[aria-selected="true"] .skill-icon .skill-icon__tile--2 { animation-name: skill-tile-2; }
  .pill[aria-selected="true"] .skill-icon .skill-icon__tile--3 { animation-name: skill-tile-3; }
  .pill[aria-selected="true"] .skill-icon .skill-icon__tile--4 { animation-name: skill-tile-4; }

  /* UX Design: round the corner, circle → top → right → home */
  .pill[aria-selected="true"] .skill-icon .skill-icon__shape--circle { animation-name: skill-shuffle-circle; }
  .pill[aria-selected="true"] .skill-icon .skill-icon__shape--triangle { animation-name: skill-shuffle-triangle; }
  .pill[aria-selected="true"] .skill-icon .skill-icon__shape--square { animation-name: skill-shuffle-square; }

  /* Accessibility: arms pivot at the shoulders */
  .skill-icon__ring { transform-origin: 10px 10px; }
  .skill-icon__arm { transform-origin: 10.3px 7.3px; }
  .pill[aria-selected="true"] .skill-icon .skill-icon__ring { animation-name: skill-ring-turn; animation-timing-function: linear; } /* turns without stopping */
  .pill[aria-selected="true"] .skill-icon .skill-icon__person { animation-name: skill-person-lift; }
  .pill[aria-selected="true"] .skill-icon .skill-icon__arm--l { animation-name: skill-arm-left; }
  .pill[aria-selected="true"] .skill-icon .skill-icon__arm--r { animation-name: skill-arm-right; }

  /* Emerging Tech */
  .skill-icon__spark--big { transform-origin: 10px 10px; }
  .skill-icon__spark--top { transform-origin: 5.24px 4.95px; }
  .skill-icon__spark--bottom { transform-origin: 5.85px 14.33px; }
  .pill[aria-selected="true"] .skill-icon .skill-icon__spark--big { animation-name: skill-spark-big; }
  .pill[aria-selected="true"] .skill-icon .skill-icon__spark--top { animation-name: skill-spark-top; }
  .pill[aria-selected="true"] .skill-icon .skill-icon__spark--bottom { animation-name: skill-spark-bottom; animation-delay: calc(var(--skill-icon-time) * -.4); } /* out of step with the top one, so one is always moving */
}

:root[data-motion="paused"] .skill-icon * {
  animation: none !important;
}

/* Every loop runs straight into the next: the only rests are short, even beats between
   steps, the same length at the end as in the middle, so the seam doesn't show */
@keyframes skill-tile-1 {
  0%, 100% { transform: none; }
  28%, 38% { transform: translate(0, -5.6px) scale(0.5); }
  68% { transform: scale(1.06); }
}

@keyframes skill-tile-2 {
  0%, 100% { transform: none; opacity: 0; }
  10%, 40% { opacity: 1; }
  58% { opacity: 0; }
  28%, 38% { transform: translate(5.6px, 0) scale(0.5); }
  68% { transform: scale(1.06); }
}

@keyframes skill-tile-3 {
  0%, 100% { transform: none; opacity: 0; }
  10%, 40% { opacity: 1; }
  58% { opacity: 0; }
  28%, 38% { transform: translate(0, 5.6px) scale(0.5); }
  68% { transform: scale(1.06); }
}

@keyframes skill-tile-4 {
  0%, 100% { transform: none; opacity: 0; }
  10%, 40% { opacity: 1; }
  58% { opacity: 0; }
  28%, 38% { transform: translate(-5.6px, 0) scale(0.5); }
  68% { transform: scale(1.06); }
}

/* Spots: circle (4.6, 15.4), triangle (10.9, 5), square (15.6, 15.2). Three moves, each
   followed by the same short beat, home included. */
@keyframes skill-shuffle-circle {
  0%, 100% { transform: none; }
  27%, 33% { transform: translate(6.3px, -10.4px); }
  60%, 67% { transform: translate(11px, -0.2px); }
  93% { transform: none; }
}

@keyframes skill-shuffle-triangle {
  0%, 100% { transform: none; }
  27%, 33% { transform: translate(4.7px, 10.2px); }
  60%, 67% { transform: translate(-6.3px, 10.4px); }
  93% { transform: none; }
}

@keyframes skill-shuffle-square {
  0%, 100% { transform: none; }
  27%, 33% { transform: translate(-11px, 0.2px); }
  60%, 67% { transform: translate(-4.7px, -10.2px); }
  93% { transform: none; }
}

@keyframes skill-ring-turn {
  from { transform: none; }
  to { transform: rotate(360deg); }
}

/* Arms up (with a little lift), down by the sides, up again with a wave, then straight
   back into the next "arms up" */
@keyframes skill-person-lift {
  0%, 100% { transform: none; }
  16% { transform: translateY(-0.6px); }
  28%, 40% { transform: none; }
  52%, 80% { transform: translateY(-0.6px); }
}

@keyframes skill-arm-left {
  0%, 100% { transform: none; }
  16% { transform: rotate(32deg); }
  34% { transform: rotate(-55deg); }
  52%, 80% { transform: rotate(32deg); }
}

@keyframes skill-arm-right {
  0%, 100% { transform: none; }
  16% { transform: rotate(-32deg); }
  34% { transform: rotate(55deg); }
  52% { transform: rotate(-32deg); }
  61% { transform: rotate(-10deg); }
  70% { transform: rotate(-32deg); }
  80% { transform: rotate(-20deg); }
}

/* Two flares, the second turning back the other way */
@keyframes skill-spark-big {
  0%, 100% { transform: none; }
  20% { transform: scale(1.12) rotate(25deg); }
  38% { transform: scale(0.92) rotate(40deg); }
  56% { transform: scale(1.1) rotate(10deg); }
  76% { transform: scale(0.97) rotate(-4deg); }
}

/* The small ones blink twice each, spinning a little as they pop back, evenly spread
   through the loop (the bottom one runs out of step with the top one) */
@keyframes skill-spark-top {
  0%, 100% { transform: none; }
  15%, 55% { transform: scale(0); }
  27%, 67% { transform: scale(1.5) rotate(45deg); }
  40%, 80% { transform: rotate(90deg); }
}

@keyframes skill-spark-bottom {
  0%, 100% { transform: none; }
  15%, 55% { transform: scale(0); }
  27%, 67% { transform: scale(1.5) rotate(-45deg); }
  40%, 80% { transform: rotate(-90deg); }
}
.skill-desc { max-width: 400px; color: var(--muted); }

/* About */
.about {
  display: grid; grid-template-columns: minmax(0, 520px) minmax(0, 695px); justify-content: space-between;
  column-gap: 48px;
  align-items: center;
  padding: 140px var(--gutter);
  border-top: 1px solid var(--line);
}
.about-text { display: grid; gap: 56px; }
.about-intro { display: grid; gap: 14px; }
.about-intro p:not(.label) { font-size: 16px; line-height: 24px; color: var(--muted); }
.about-title { font: 400 72px/72px var(--serif); letter-spacing: -.03em; margin-bottom: 10px; }
.about .label { margin-bottom: 10px; } /* 24px to the title, with the 14px gap (Figma Intro) */
.career { border-bottom: 1px solid var(--line); }
.career li {
  display: grid; grid-template-columns: 1fr auto 40px; gap: 16px; align-items: center;
  padding: 16px 0;
  border-top: 1px solid var(--line);
  position: relative;
  transition: translate .4s cubic-bezier(.5, 0, 0, 1);
}
/* The company link stretches over its whole row, so a click anywhere on it opens their site */
.career-co::after { content: ""; position: absolute; inset: 0; }
/* Like bruce-cao.com: the row you're on slides over and the portrait changes */
.career li.is-active { translate: 12px 0; }
.career-text { display: grid; }
.career-co { justify-self: start; font-weight: 500; font-size: 14px; line-height: 24px; color: inherit; text-decoration: none; } /* a link to the company, as wide as its name */
.career-role { color: var(--muted); }
.career-when { font: 500 12px/20px var(--mono); letter-spacing: .04em; color: var(--muted); }
.career-logo { width: 40px; height: 40px; color: var(--ink); } /* 8px corners on its hairline tile */
/* Each company's mark in ink inside a hairline tile, like the work tiles' border */
.career-logo__tile { fill: none; stroke: var(--line); stroke-width: 1; vector-effect: non-scaling-stroke; }

/* Dot portrait (after bruce-cao.com) */
.dots-portrait { overflow: hidden; border-radius: 16px; }
/* The photo fits the window: its height minus an 80px gap above and below */
.dots-canvas { display: block; width: 100%; height: calc(100svh - 160px); min-height: 480px; }

/* Words */
.words {
  display: flex; flex-direction: column; align-items: center; gap: 96px;
  padding: 140px var(--gutter);
  border-top: 1px solid var(--line);
}
/* The label over the title, 16px above it (Figma Words "Heading") */
.words-heading { display: grid; justify-items: center; gap: 16px; }
.section-title {
  max-width: 519px; text-align: center;
  font: 400 clamp(44px, 5.6vw, 80px)/1 var(--serif); letter-spacing: -.03em;
}
/* After svsd.school: a short serif pull line over a small sans paragraph */
.quotes { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 58px; width: 100%; align-items: start; }
.quotes li { padding-top: 28px; border-top: 1px solid var(--line); }
.quotes figure { display: grid; gap: 16px; }
/* Side by side, the pull lines, paragraphs and names share rows so they line up across */
@media (min-width: 1024px) {
  .quotes li, .quotes figure { display: grid; grid-row: span 3; grid-template-rows: subgrid; }
}
.pull { font: 400 28px/32px var(--serif); letter-spacing: -.01em; }
.quotes blockquote { font-size: 16px; line-height: 24px; color: var(--muted); }
.quotes figcaption { font: 500 12px/20px var(--mono); letter-spacing: .04em; color: var(--muted); } /* the role in mono, like the career dates */
.quotes .who { font: 500 14px/20px var(--sans); letter-spacing: 0; }
.who { color: var(--ink); font-weight: 500; margin-right: 6px; }
/* Two sets of three stacked in one spot, as tall as the taller set, so swapping never
   moves the page; the one not showing is hidden and inert (app.js) */
.quotes-wrap { display: grid; justify-items: center; gap: 56px; width: 100%; }
.quote-sets { display: grid; width: 100%; }
.quote-sets > .quotes { grid-area: 1 / 1; }
.quotes.is-off { visibility: hidden; }
/* "More receipts": a muted link like "Video description" (the nav's 12/24, Figma
   Type/link/nav), with a fine ↻ that turns once */
.quotes-more {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 0; border: 0; background: none; cursor: pointer;
  font: 400 12px/24px var(--sans); color: var(--muted);
}
.quotes-more__icon { width: 13px; height: 13px; }

/* Contact */
.contact {
  display: flex; flex-direction: column; align-items: center; gap: 96px;
  padding: 140px var(--gutter) 80px;
  text-align: center;
  border-top: 1px solid var(--line);
  overflow-x: clip; /* the people run off at the window's edges, without widening the page */
}
.contact-message { display: flex; flex-direction: column; align-items: center; gap: 20px; }
.contact-title { max-width: 715px; font: 400 clamp(56px, 8.9vw, 128px)/.9375 var(--serif); letter-spacing: -.04em; }
.contact-actions { display: flex; flex-direction: column; align-items: center; gap: 32px; }
.contact-sub { color: var(--muted); }
.email-btn {
  display: inline-flex; align-items: center; gap: 8px;
  height: 48px; padding: 0 20px;
  border-radius: 999px;
  background: var(--ink); color: var(--ink-inverse);
  font: 400 14px/20px var(--sans); /* Type/body/s, like every button */
  text-decoration: none;
  transition: background-color .2s var(--ease), scale .15s var(--ease);
}

.contact-buttons { display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; }
/* Secondary button (Figma "Secondary button"): outlined, Type/body/s label, a fine play icon
   that becomes a fine cross while the video is open */
:is(.video-btn, .secondary-btn) {
  display: inline-flex; align-items: center; gap: 8px;
  height: 48px; padding: 0 20px;
  border: 1px solid var(--ink); border-radius: 999px;
  background: var(--bg); color: var(--ink);
  font: 400 14px/20px var(--sans); /* Type/body/s, like every button */
  cursor: pointer;
  transition: background-color .2s var(--ease), scale .15s var(--ease);
}
.video-btn.is-open .video-btn__play, .video-btn:not(.is-open) .video-btn__close { display: none; }

/* Fine-line icons, drawn at Plex's stroke weight */
.line-icon {
  flex: none; width: 14px; height: 14px; overflow: visible;
  fill: none; stroke: currentColor; stroke-width: 1.25; stroke-linecap: round; stroke-linejoin: round;
}

/* Video (Figma Contact "Video"): shares the people's spot, so swapping them never moves the page */
/* As on the live site, the people never shrink below 600px wide: on narrower screens the
   row stays centred and runs off both edges (.contact clips it) */
.people-wrap { display: grid; grid-template-columns: minmax(0, 1fr); justify-items: center; min-width: 600px; }
.people, .video-stage { grid-area: 1 / 1; }
.video-stage { display: grid; align-content: start; gap: 12px; width: 65.5%; visibility: hidden; }
.people-wrap.is-video .video-stage { visibility: visible; }
.people-wrap.is-video .people { visibility: hidden; }
.video-box { position: relative; }
.video-frame {
  position: relative; aspect-ratio: 16 / 9; overflow: hidden; isolation: isolate;
  border: 1px solid var(--line); border-radius: 16px;
  background: var(--surface); cursor: pointer;
}
.video-frame video {
  display: block; width: 100%; height: 100%; object-fit: cover;
  border-radius: inherit; /* browsers draw video on its own layer and can ignore the frame's rounded clip */
}
/* The thumbnail rests as dots, with Play over them. Pressing Play solidifies it into the
   video, and the controls fade in after. While the dots form or scatter, the player hides. */
.video-player { position: relative; z-index: 1; }
.video-stage.is-forming .video-player { opacity: 0; }
/* The description link fades in once the dots have formed, and out as they scatter */
.video-desc { transition: opacity .4s ease; }
.people-wrap:not(.is-video) .video-desc, .video-stage.is-forming .video-desc { opacity: 0; }
@media (prefers-reduced-motion: reduce) { .video-desc { transition: none; } }
.video-frame { transition: border-color .6s ease, background-color .6s ease; }
.video-frame video { transition: opacity .6s ease; }
.video-seek, .video-controls { transition: opacity .4s ease .3s; }
.video-stage.is-dotted .video-frame { border-color: transparent; background-color: transparent; }
.video-stage.is-dotted .video-frame video { opacity: 0; }
.video-stage.is-dotted :is(.video-seek, .video-controls) { opacity: 0; visibility: hidden; transition: none; }
.video-dots {
  position: absolute; inset: -80px; width: calc(100% + 160px); height: calc(100% + 160px);
  pointer-events: none;
}

/* Before the first play: the site's main button (the email button's style) on the thumbnail */
.video-start {
  position: absolute; top: 50%; left: 50%; z-index: 1; translate: -50% -50%;
  display: inline-flex; align-items: center; gap: 8px;
  height: 48px; padding: 0 20px;
  border: 0; border-radius: 999px;
  background: var(--ink); color: var(--ink-inverse);
  font: 400 14px/20px var(--sans); /* Type/body/s, like every button */
  cursor: pointer;
  transition: opacity .3s ease, background-color .2s var(--ease), scale .15s var(--ease);
}
.video-start__length { opacity: .6; letter-spacing: 0; font-variant-numeric: tabular-nums; }
.video-start__length:empty { display: none; }
/* Hover and pressed, the same for every button: filled ones soften a little towards the
   page colour, outlined ones (and the skill pills) take a faint ink tint, and pressing
   pushes them in. No shadows or glow. */
@media (hover: hover) {
  :is(.email-btn, .video-start):hover { background-color: color-mix(in srgb, var(--ink) 84%, var(--bg)); }
  :is(.video-btn, .secondary-btn, .pill[aria-selected="false"]):hover { background-color: color-mix(in srgb, var(--ink) 8%, var(--bg)); }
  .pill[aria-selected="true"]:hover { background-color: color-mix(in srgb, var(--ink) 84%, var(--bg)); }
}
:is(.email-btn, .video-start, .pill[aria-selected="true"]):active { background-color: color-mix(in srgb, var(--ink) 72%, var(--bg)); }
:is(.video-btn, .secondary-btn, .pill[aria-selected="false"]):active { background-color: color-mix(in srgb, var(--ink) 14%, var(--bg)); }
:is(.email-btn, .video-btn, .secondary-btn, .video-start, .pill):active { scale: .96; }
.video-player[data-started] .video-start { opacity: 0; visibility: hidden; transition: opacity .3s ease, visibility 0s .3s; }

/* Under the frame: a hairline that fills as it plays, then one row of quiet text controls */
.video-seek {
  --played: 0%;
  display: block; width: 100%; height: 20px; margin: 4px 0 0;
  background: none; cursor: pointer; appearance: none; -webkit-appearance: none;
}
.video-seek::-webkit-slider-runnable-track { height: 1px; background: linear-gradient(to right, var(--ink) var(--played), var(--line) var(--played)); }
.video-seek::-moz-range-track { height: 1px; background: linear-gradient(to right, var(--ink) var(--played), var(--line) var(--played)); }
.video-seek::-webkit-slider-thumb { width: 9px; height: 9px; margin-top: -4px; border: 0; border-radius: 50%; background: var(--ink); appearance: none; -webkit-appearance: none; transition: scale .15s ease; }
.video-seek::-moz-range-thumb { width: 9px; height: 9px; border: 0; border-radius: 50%; background: var(--ink); transition: scale .15s ease; }
.video-seek:active::-webkit-slider-thumb { scale: 1.4; }
.video-seek:active::-moz-range-thumb { scale: 1.4; }
.video-seek:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; border-radius: 4px; }
.video-controls {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 4px 16px;
  font: 500 12px/20px var(--mono); letter-spacing: .04em; /* Type/label/meta: the video controls stay mono */
  color: var(--muted);
}
.video-controls__group { display: flex; align-items: center; gap: 16px; }
.vctl {
  display: inline-flex; align-items: center; gap: 6px; min-height: 24px; /* a 24px target (WCAG 2.5.8) */
  padding: 0; border: 0; background: none;
  color: inherit; font: inherit; letter-spacing: inherit;
  cursor: pointer;
}
.vctl[hidden] { display: none; }
/* The audio description toggle: a small dot, filled while it's on */
.vctl--toggle::before { content: ""; width: 5px; height: 5px; border: 1px solid currentColor; border-radius: 50%; }
.vctl--toggle[aria-pressed="true"] { color: var(--ink); }
.vctl--toggle[aria-pressed="true"]::before { background: currentColor; }
.video-time { font-variant-numeric: tabular-nums; }
/* Full screen: the film fills the screen with the controls along the bottom */
.video-player:fullscreen { display: flex; flex-direction: column; justify-content: center; gap: 0; padding: 16px 24px; background: var(--bg); }
.video-player:fullscreen .video-frame { flex: 1 1 auto; min-height: 0; aspect-ratio: auto; border: 0; border-radius: 0; background: var(--bg); }
.video-player:fullscreen .video-frame video { object-fit: contain; }

/* Video description: a muted link like the nav's (12px, underline on hover), led by a fine
   + like the lab's other icons. Opening it turns the + 's upright stroke flat into a −, with
   the career rows' easing */
.video-desc { font-size: 14px; line-height: 20px; }
.video-desc summary {
  display: flex; align-items: center; gap: 6px; width: fit-content; margin: 0 auto;
  font-size: 12px; line-height: 24px; /* the nav links' size */
  color: var(--muted); cursor: pointer; list-style: none;
}
.video-desc summary::-webkit-details-marker { display: none; }
.fold-icon { width: 13px; height: 13px; }
.fold-icon__bar { transform-box: view-box; transform-origin: 8px 8px; transition: rotate .4s cubic-bezier(.5, 0, 0, 1); }
.video-desc.is-open .fold-icon__bar { rotate: 90deg; }
.video-desc__body { display: grid; gap: 8px; padding-top: 20px; color: var(--muted); text-align: left; }
.video-desc__body strong { color: var(--ink); font-weight: 500; }
.video-desc__body strong:first-child { display: block; }
.video-desc__body ul { display: grid; gap: 4px; margin: 0; padding: 0; list-style: none; }
@media (prefers-reduced-motion: reduce) { .fold-icon__bar, .video-start { transition: none; } }

/* People drawing (ported from the live site's hero): 2400×889, uncropped */
.people-wrap { position: relative; width: min(760px, 100%); }
.people { position: relative; width: 100%; aspect-ratio: 2400 / 889; }
.people-stage { position: absolute; inset: 0; }
.person {
  position: absolute; top: 0;
  left: var(--left); width: var(--width); height: 100%;
  transform-origin: var(--origin);
  -webkit-tap-highlight-color: transparent;
}
.person svg { display: block; width: 100%; height: 100%; overflow: visible; transform-origin: 50% 90%; }
.person__line, .hero-doodle .line { fill: var(--ink); }
.person__shade, .hero-doodle .shade { fill: var(--shadow); }
.person .person__lip { fill: var(--bg); }
.person__pupil { transition: transform .22s cubic-bezier(.3, .7, .4, 1); }
.person__eye, .person__brow, .person__lip { transform-box: fill-box; transform-origin: center; }
.person__brow { transition: transform .3s ease; }

/* The hero head is one drawing on its own, so it isn't placed in a strip */
.hero-doodle.person { position: relative; display: block; left: auto; top: auto; width: 44px; height: 92px; }
/* The hero boy blinks now and then (sometimes twice) and raises his eyebrows on a
   different beat, so the two rarely line up. Paused or Reduce motion: eyes open, still. */
.hero-doodle :is(.eye, .brow) { transform-box: fill-box; transform-origin: center; }
@media (prefers-reduced-motion: no-preference) {
  :root:not([data-motion="paused"]) .hero-doodle .eye { animation: doodle-blink 5.2s ease-in-out 2.4s infinite; }
  :root:not([data-motion="paused"]) .hero-doodle .brow { animation: doodle-brow 7.4s var(--ease) 3.6s infinite; }
}
@keyframes doodle-blink {
  0%, 44%, 47.5%, 92%, 95.5%, 97%, 100% { scale: 1 1; }
  45.5%, 93.5%, 98.5% { scale: 1 .12; }
}
@keyframes doodle-brow {
  0%, 30%, 52%, 100% { translate: 0 0; rotate: 0deg; }
  34%, 46% { translate: 0 -1.6px; rotate: -6deg; }
}

/* Speech bubble: pops out of the 6th person while they're hovered or tapped.
   Tail tip lands just above their head (75% across, 24.8% down the uncropped drawing). */
.speech-bubble {
  --tip-x: 75%;
  --tip-y: 24.8%;
  position: absolute;
  left: calc(var(--tip-x) - 12.3% * .827);
  bottom: calc(100% - var(--tip-y));
  width: 12.3%;
  overflow: visible; pointer-events: none;
  opacity: 0;
  transform: scale(.3) rotate(-8deg); transform-origin: 82.7% 100%;
  transition: opacity .15s ease, transform .2s ease-in;
}
.speech-bubble__fill { fill: var(--bg); }
.speech-bubble__line { fill: var(--ink); stroke: var(--ink); stroke-width: 1.8; stroke-linejoin: round; }
.speech-bubble__text { fill: var(--muted); }
.people-wrap.is-talking .speech-bubble {
  opacity: 1; transform: none;
  transition: opacity .12s ease, transform .45s cubic-bezier(.34, 1.56, .64, 1);
}
@media (hover: hover) {
  .people-wrap:has(.person:nth-child(6):hover) .speech-bubble {
    opacity: 1; transform: none;
    transition: opacity .12s ease, transform .45s cubic-bezier(.34, 1.56, .64, 1);
  }
}

/* Drawings' life, as on the live site: they pop up when they first come on screen,
   each has its own idle loop, someone random hops every few seconds, a hover (or tap)
   makes them jump. Pausing holds every pose; Reduce motion keeps them still. */
:root[data-motion="paused"] .person { animation: var(--idle-loop); animation-play-state: paused; }
@media (prefers-reduced-motion: no-preference) {
  .person {
    --idle-loop: var(--idle) ease-in-out var(--delay, 0s) infinite;
    animation: var(--idle-loop);
  }
  .people:not(.is-settled) .person,
  .hero:not(.is-settled) .hero-doodle {
    animation: var(--idle-loop), person-enter .75s cubic-bezier(.3, .7, .4, 1) calc(.3s + var(--enter, 0s)) backwards;
  }
  /* The people wait below their frame until they scroll into view */
  .people:not(.is-shown) .person { animation-play-state: paused; }
  .hero-doodle { --idle: person-sway 4.6s; --enter: 0s; }
  .people-stage .person:nth-child(1) { --idle: person-sway 4.2s; --enter: .12s; }
  .people-stage .person:nth-child(2) { --idle: person-breathe 3.1s; --delay: -1.2s; --enter: .3s; }
  .people-stage .person:nth-child(3) { --idle: person-bob 3.4s; --delay: -1.7s; --enter: 0s; }
  .people-stage .person:nth-child(4) { --idle: person-sway 5.7s; --delay: -2.1s; --enter: .2s; }
  .people-stage .person:nth-child(5) { --idle: person-bob 2.9s; --delay: -.6s; --enter: .36s; }
  .people-stage .person:nth-child(6) { --idle: person-lean 6.1s; --delay: .8s; --enter: .06s; }
  .people-stage .person:nth-child(7) { --idle: person-breathe 3.7s; --delay: -2.4s; --enter: .26s; }
  .person.is-hopping { animation: var(--idle-loop), person-hop .9s ease-in-out; }
  .person.is-jumping { animation: var(--idle-loop), person-jump .6s ease-out; }
  @media (hover: hover) {
    .person:hover { animation: var(--idle-loop), person-jump .6s ease-out; }
  }
  .person__eye.is-blinking { animation: person-blink .18s ease-in-out; }
  .person.is-surprised .person__brow { transform: translateY(-9px); }
  :root:not([data-motion="paused"]) .people-wrap.is-talking .person__lip--bottom { animation: person-talk .16s ease-in-out infinite alternate; }
  :root:not([data-motion="paused"]) .people-wrap.is-talking .person__lip--top { animation: person-talk-top .16s ease-in-out infinite alternate; }
  @media (hover: hover) {
    :root:not([data-motion="paused"]) .people-wrap:has(.person:nth-child(6):hover) .person__lip--bottom { animation: person-talk .16s ease-in-out infinite alternate; }
    :root:not([data-motion="paused"]) .people-wrap:has(.person:nth-child(6):hover) .person__lip--top { animation: person-talk-top .16s ease-in-out infinite alternate; }
  }
}
@keyframes person-sway { 0%, 100% { transform: rotate(-1.5deg); } 50% { transform: rotate(1.5deg); } }
@keyframes person-breathe { 0%, 100% { transform: scale(1, 1); } 50% { transform: scale(1.015, 1.03); } }
@keyframes person-bob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-1.2%); } }
@keyframes person-lean { 0%, 60%, 100% { transform: rotate(0); } 20%, 40% { transform: rotate(-2.5deg); } }
@keyframes person-enter {
  0% { translate: 0 100%; scale: 1 1; opacity: 0; }
  20% { opacity: 1; }
  65% { translate: 0 -3%; scale: .98 1.03; }
  85% { translate: 0 0; scale: 1.02 .98; }
  100% { translate: 0 0; scale: 1 1; }
}
@keyframes person-hop {
  0%, 100% { translate: 0 0; scale: 1 1; }
  20% { translate: 0 0; scale: 1.04 .96; }
  50% { translate: 0 -4%; scale: .98 1.03; }
  80% { translate: 0 0; scale: 1.03 .97; }
}
@keyframes person-jump {
  0%, 100% { translate: 0 0; scale: 1 1; }
  15% { translate: 0 0; scale: 1.05 .95; }
  45% { translate: 0 -7%; scale: .97 1.04; }
  80% { translate: 0 0; scale: 1.04 .96; }
}
@keyframes person-blink { 0%, 100% { transform: scaleY(1); } 50% { transform: scaleY(.1); } }
@keyframes person-talk { from { transform: translateY(0); } to { transform: translateY(7px); } }
@keyframes person-talk-top { from { transform: translateY(0); } to { transform: translateY(-2px); } }

/* Footer */
.foot {
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px 24px;
  padding: 24px var(--gutter);
  font-size: 12px; line-height: 18px; color: var(--muted);
}
.foot-controls { grid-column: 1 / -1; justify-self: center; display: flex; gap: 8px; margin-top: 16px; }
/* The day, live time and place, said as a terminal would (home: typed in, fun.js) */
.foot .foot-date {
  grid-column: 1 / -1; justify-self: center; text-align: center; margin-top: 4px;
  font: 500 12px/20px var(--mono); letter-spacing: .04em; white-space: nowrap; /* Type/label/meta */
}
.foot-date [data-type]:first-child { color: var(--ink); }
.foot-date .clock { font-variant-numeric: tabular-nums; }
/* Desktop, in the same order as phones: what the site is and where I am across the top
   (the sentences left and centre, the date right), then the controls, then the copyright */
@media (min-width: 768px) {
  .foot p:nth-child(2) { grid-area: 1 / 1; }
  .foot p:nth-child(3) { grid-area: 1 / 2; text-align: center; }
  .foot .foot-date { grid-area: 1 / 3; justify-self: end; text-align: right; margin-top: -1px; } /* on the sentences' baseline */
  .foot-controls { grid-row: 2; }
  .foot p:nth-child(1) { grid-area: 3 / 1 / 4 / -1; justify-self: center; margin-top: 8px; }
}

/* Each tile's XL art (live site, css/styles.css). Multi-brand and Plusnet are re-exported
   from the Figma tiles with the frame filled in this page's --bg (white / #0a0a0a) in place
   of the grey; Debug's art is already see-through. */
.work-art--multi { --stage-w: 1200px; --tile-light: url("../assets/work-multi-xl-light.png"); --tile-dark: url("../assets/work-multi-xl-dark.png"); }
.work-art--debug { --stage-w: 588px; --tile-light: url("../assets/work-debug-xl-light.webp"); --tile-dark: url("../assets/work-debug-xl-dark.webp"); }
.work-art--plusnet { --stage-w: 588px; --tile-light: url("../assets/work-plusnet-xl-light.png"); --tile-dark: url("../assets/work-plusnet-xl-dark.png"); }

/* ---------- Multi-brand tile ----------
   The BT · EE · Plusnet tile in the Multi-brand artwork is redrawn in code exactly on top
   of it, so it can play once as it comes onto the screen (app.js adds .is-ready, then
   .is-playing): one larger BT logo sits in the middle, and EE and Plusnet slide out from
   behind it as it settles to its size, all three in Loop blue; then a cursor drags the
   dotted selection box round them, as in a design program, and each logo takes its brand
   colour as the box touches it (app.js adds .is-selected), before the cursor slips away.
   It ends exactly as the artwork looks.
   Used on the homepage's wide card (.brands; Figma "Large - Multi - Light/Dark - XS–XL"
   › "Brands (BT · EE · Plusnet)").
   With Reduce motion on, animations paused, or no JavaScript, it's simply the artwork.
   Positions are in the artwork's own pixels (it's shown at designed size, centred and
   bottom-anchored, like the card background); x/y are from the tile's top-left. */
.brands {
  /* XS: logos in a row */
  --art-w: 535px;
  --fx: 99px;
  --fy: 336px;
  --fw: 336px;
  --fh: 112px;
  --box-x: 16px;
  --box-y: 16px;
  --box-w: 304px;
  --box-h: 80px;
  --logo: 57.6px;
  --bt-x: 49.6px;
  --bt-y: 27.2px;
  --ee-x: 139.2px;
  --ee-y: 27.2px;
  --pn-x: 228.8px;
  --pn-y: 27.2px;
  --bt-start: 1.3; /* how much bigger BT starts */
  --dark-shift: 0px;
  --brands-bg: #e7edfa;
  --brands-fill: #c5d5f4;
  position: absolute;
  left: calc(50% - var(--art-w) / 2 + var(--fx) + var(--dark-shift));
  bottom: calc(560px - var(--fy) - var(--fh) - 1px); /* the card's 1px border */
  width: var(--fw);
  height: var(--fh);
  pointer-events: none;
}

:root[data-theme="dark"] .brands {
  --brands-bg: #0d1930;
  --brands-fill: #091120;
}

/* S, both versions: the dark artwork's tile sits 2px further left */
@media (min-width: 568px) and (max-width: 767px) {
  :root[data-theme="dark"] .brands {
    --dark-shift: -2px;
  }
}

@media (min-width: 568px) {
  .brands {
    --art-w: 735px;
    --fx: 108px;
    --fy: 309px;
    --fw: 376px;
    --fh: 126px;
    --box-x: 18px;
    --box-y: 18px;
    --box-w: 340px;
    --box-h: 90px;
    --logo: 64.5px;
    --bt-x: 55.5px;
    --bt-y: 30.6px;
    --ee-x: 155.8px;
    --ee-y: 30.6px;
    --pn-x: 256px;
    --pn-y: 30.6px;
  }
}

@media (min-width: 768px) {
  .brands {
    --art-w: 973px;
    --fx: 273px;
    --fy: 280px;
    --fw: 420px;
    --fh: 140px;
    --box-x: 20px;
    --box-y: 20px;
    --box-w: 380px;
    --box-h: 100px;
    --logo: 72px;
    --bt-x: 62px;
    --bt-y: 34px;
    --ee-x: 174px;
    --ee-y: 34px;
    --pn-x: 286px;
    --pn-y: 34px;
  }
}

/* L and XL: a square tile, BT on top of EE and Plusnet */
@media (min-width: 1024px) {
  .brands {
    --art-w: 1089px;
    --fx: 529px;
    --fy: 140px;
    --fw: 280px;
    --fh: 280px;
    --box-w: 240px;
    --box-h: 240px;
    --logo: 88px;
    --bt-x: 96px;
    --bt-y: 50px;
    --ee-x: 46px;
    --ee-y: 138px;
    --pn-x: 145px;
    --pn-y: 138px;
    --bt-start: 1.6;
  }
}

@media (min-width: 1140px) {
  .brands {
    --art-w: 1200px;
    --fx: 640px;
  }
}

/* Hides the artwork's own logos and box (the same colour as the tile around them) */
.brands__cover {
  position: absolute;
  left: calc(var(--box-x) - 4px);
  top: calc(var(--box-y) - 4px);
  width: calc(var(--box-w) + 8px);
  height: calc(var(--box-h) + 8px);
  background: var(--brands-bg);
}

.brands__box {
  position: absolute;
  left: var(--box-x);
  top: var(--box-y);
  width: var(--box-w);
  height: var(--box-h);
  overflow: visible; /* the dashes sit centred on the edge */
}

.brands__rect {
  fill: var(--brands-fill);
  stroke: #3768c7;
  stroke-linecap: round;
}

.brands__rect--square { display: none; }

@media (min-width: 1024px) {
  .brands .brands__rect--square { display: inline; }
  .brands .brands__rect--wide { display: none; }
}

.brands__logo {
  position: absolute;
  width: var(--logo);
  height: var(--logo);
}

.brands__logo--bt { left: var(--bt-x); top: var(--bt-y); --x: var(--bt-x); --y: var(--bt-y); }
.brands__logo--ee { left: var(--ee-x); top: var(--ee-y); --x: var(--ee-x); --y: var(--ee-y); }
.brands__logo--plusnet { left: var(--pn-x); top: var(--pn-y); --x: var(--pn-x); --y: var(--pn-y); }

/* The drag cursor's tip starts at the box's top-left corner */
.brands__cursor {
  position: absolute;
  left: calc(var(--box-x) - 1.5px);
  top: calc(var(--box-y) - 1.5px);
  width: 16px;
  height: 22px;
  opacity: 0;
}

@media (prefers-reduced-motion: no-preference) {
  /* Waiting at the start from the first paint (the <head> script adds .motion when
     animations are on; app.js adds .is-ready too, for turning animations back on later),
     so nothing changes colour before it plays.
     0.25s rest, then the split (0.6s, the site's swap timing), then the drag */
  :is(.motion .brands, .brands.is-ready) .brands__logo {
    animation: brands-split 0.6s cubic-bezier(0.45, 0, 0.2, 1) 0.25s both;
  }

  /* Loop blue until the selection box reaches each logo, then a quick fade to its own
     colour (the fade is only on the way to selected, so going blue is instant) */
  :root:not([data-motion="paused"]) :is(.motion .brands, .brands.is-ready):not(.is-done) .brands__logo:not(.is-selected) .brands__fill {
    fill: #3768c7; /* Loop blue, as on the "BT Loop Design" tile */
  }

  .brands__logo.is-selected .brands__fill {
    transition: fill 0.3s ease-out;
  }

  :is(.motion .brands, .brands.is-ready) .brands__box {
    animation: brands-drag 1.6s 0.85s both;
  }

  :is(.motion .brands, .brands.is-ready) .brands__cursor {
    animation: brands-cursor 1.6s 0.85s both;
  }

  :is(.motion .brands, .brands.is-ready):not(.is-playing) * {
    animation-play-state: paused;
  }
}

/* Done, paused or never started: just the artwork */
.brands.is-done *,
:root[data-motion="paused"] .brands * {
  animation: none !important;
}

/* Each logo starts in the middle of the box, EE and Plusnet tucked behind BT (a little
   smaller), and slides out to its place */
@keyframes brands-split {
  from {
    translate: calc(var(--box-x) + var(--box-w) / 2 - var(--logo) / 2 - var(--x))
      calc(var(--box-y) + var(--box-h) / 2 - var(--logo) / 2 - var(--y));
    scale: 0.85;
  }
}

:is(.motion .brands, .brands.is-ready) .brands__logo--bt { animation-name: brands-split-bt; }

/* BT starts larger (1.3×, or 1.6× on L and XL; still inside where the box will be) and
   settles to its size as it moves into place */
@keyframes brands-split-bt {
  from {
    translate: calc(var(--box-x) + var(--box-w) / 2 - var(--logo) / 2 - var(--x))
      calc(var(--box-y) + var(--box-h) / 2 - var(--logo) / 2 - var(--y));
    scale: var(--bt-start);
  }
}

/* The cursor fades in at the corner, drags the box out, then fades away */
@keyframes brands-drag {
  0%,
  15% { width: 0; height: 0; animation-timing-function: cubic-bezier(0.45, 0, 0.25, 1); }
  70%,
  100% { width: var(--box-w); height: var(--box-h); }
}

@keyframes brands-cursor {
  0% { opacity: 0; translate: 0 0; }
  15% { opacity: 1; translate: 0 0; animation-timing-function: cubic-bezier(0.45, 0, 0.25, 1); }
  70% { opacity: 1; translate: var(--box-w) var(--box-h); }
  100% { opacity: 0; translate: var(--box-w) var(--box-h); }
}

/* ---------- Debug tile ----------
   The Debug tile's split card (Figma "Tile / Debug / XS–XL" › "Split card": the iPhone
   card in normal mode on the left of the blue divider, debug mode on the right inside
   the purple dotted outline) is redrawn in code exactly on top of the artwork, so it can
   play once as it comes onto the screen (app.js adds .is-playing): the divider slides
   to the card's left edge, showing it all in debug mode, across past its right edge and
   over the outline, showing it all in normal mode with no outline, and back to the
   middle, the outline following it.
   With Reduce motion on, animations paused, or no JavaScript, it's simply the artwork.
   The layout is the same at every size: the split sits at the artwork's centre (its
   width is odd below XL, so the split is half a pixel right of the card's centre).
   Inside, x is from 130px left of the split and y from 200px down the artwork; --cut
   is where the divider is (130px at rest, 8px at the card's left edge, 260px just past
   the outline's dots on the right). The card colour under it hides the artwork's own split card.
   Both halves line up at the top (in the artwork the taller debug card is centred on the
   normal one), so the phones sit level. */
@property --cut {
  syntax: "<length>";
  inherits: true;
  initial-value: 130px;
}

.debug-split {
  --half-px: 0.5px;
  position: absolute;
  left: calc(50% + var(--half-px) - 130px);
  bottom: -1px; /* the card's 1px border */
  width: 262px;
  height: 360px;
  overflow: hidden;
  background: var(--bg); /* lab: the page colour, where the live card is grey */
  pointer-events: none;
}

@media (min-width: 1140px) {
  .debug-split { --half-px: 0px; }
}

.debug-split__card {
  position: absolute;
  left: 8px;
  width: 244px;
  background-size: 100% 100%;
}

.debug-split__card--normal {
  top: 22px;
  height: 404px;
  background-image: url("../assets/work-debug-card-normal-light.webp");
  clip-path: inset(0 calc(252px - var(--cut)) 0 0);
}

:root[data-theme="dark"] .debug-split__card--normal {
  background-image: url("../assets/work-debug-card-normal-dark.webp");
}

.debug-split__card--debug {
  top: 22px;
  height: 419px;
  background-image: url("../assets/work-debug-card-debug.webp");
  clip-path: inset(0 0 0 calc(var(--cut) - 8px));
}

/* In debug mode the iPhone photo is an image placeholder, filling the photo area inside
   the card's blue border (5px in, 234 × 239) */
.debug-split__card--debug::before {
  content: "";
  position: absolute;
  left: 5px;
  top: 5px;
  width: 234px;
  height: 239px;
  background: url("../assets/work-debug-card-image-light.svg") center / cover;
}

/* Dark mode: the dark blue of an image in debug mode on a dark page */
:root[data-theme="dark"] .debug-split__card--debug::before {
  background-image: url("../assets/work-debug-card-image-dark.svg");
}

.debug-split__divider {
  position: absolute;
  left: calc(var(--cut) - 1px);
  top: 0;
  width: 2px;
  height: 366px;
  background: #039dff;
}

/* Dots every 8px from the split, as in Figma (the line starts 16 dots to the left so
   they stay in step), shown only to the right of the divider */
.debug-split__outline {
  position: absolute;
  inset: 0;
  overflow: visible;
  fill: none;
  stroke: #8b3dff;
  stroke-width: 3.5;
  stroke-linecap: round;
  stroke-dasharray: 0.01 8;
  clip-path: inset(-2px -2px -2px var(--cut));
}

@media (prefers-reduced-motion: no-preference) {
  /* 0.25s rest, then left (0.9s), a beat, right across the card (1.3s), a beat, and back
     to the middle (0.9s) */
  .debug-split.is-playing {
    animation: debug-split 3.6s 0.25s both;
  }
}

.debug-split.is-done,
:root[data-motion="paused"] .debug-split {
  animation: none !important;
}

@keyframes debug-split {
  0% { --cut: 130px; animation-timing-function: cubic-bezier(0.65, 0, 0.35, 1); }
  25%,
  32% { --cut: 8px; animation-timing-function: cubic-bezier(0.65, 0, 0.35, 1); }
  68%,
  75% { --cut: 260px; animation-timing-function: cubic-bezier(0.65, 0, 0.35, 1); }
  100% { --cut: 130px; }
}

/* ---------- Plusnet tile ----------
   The phone's screen on the Plusnet tile (Figma "Plusnet - Light/Dark - XS–XL" ›
   "iPhone mockup" › "Paste design here 1": the order tracker page under the status bar,
   the same in both themes) is redrawn in code exactly on top of the artwork, so the page
   can play once as it comes onto the screen (app.js adds .is-playing): it scrolls right
   down to its end (the completion date, latest update and order progress), rests, and
   scrolls back to the top, under the status bar.
   The SVG is the screen from Figma: its rounded rectangle (271.07 × 586.63, the status
   bar's vectors on top), in the same units, with the page 40.34 down. The page and the
   status bar sit inside that one shape, so their edges line up all the way round. The
   shape is 0.5 larger on every side than in Figma, to cover the artwork's soft screen
   edge (a pixel blending the frosted bezel with the white page); otherwise that light rim
   shows as a thin line beside the pink as the page scrolls. Two copies of the page sit
   1 to either side beneath it, filling that extra half with the page's edge colour.
   Positions are in the artwork's own pixels: --sx/--sy is the screen's top-left, and on
   XS the phone is drawn a little smaller (--k). With Reduce motion on, animations
   paused, or no JavaScript, the page simply rests at the top. */
.plusnet-screen {
  --art-w: 535px;
  --sx: 137.88px;
  --sy: 231.89px;
  --k: 0.95156;
  position: absolute;
  left: calc(50% - var(--art-w) / 2 + var(--sx) - 1px * var(--k));
  bottom: -1px; /* the card's 1px border */
  width: calc(273.067px * var(--k));
  height: calc(560px - var(--sy) + 1px * var(--k));
  overflow: hidden;
  pointer-events: none;
}

.plusnet-screen svg {
  display: block;
  width: 100%;
  height: auto;
}

@media (min-width: 568px) {
  .plusnet-screen { --art-w: 735px; --sx: 231.28px; --sy: 192.26px; --k: 1; }
}

@media (min-width: 768px) {
  .plusnet-screen { --art-w: 475px; --sx: 101.28px; --sy: 208.26px; }
}

@media (min-width: 1024px) {
  .plusnet-screen { --art-w: 533px; --sx: 130.28px; --sy: 220.26px; }
}

@media (min-width: 1140px) {
  .plusnet-screen { --art-w: 588px; --sx: 157.28px; --sy: 208.26px; }
}

@media (prefers-reduced-motion: no-preference) {
  /* 0.25s rest, down to the end of the page (1.2s), a pause to read (0.7s), back to the
     top (1.1s). The end is the page's height less what the tile shows: 219–247px. */
  .plusnet-screen.is-playing .plusnet-screen__page {
    animation: plusnet-scroll 3s 0.25s both;
  }
}

.plusnet-screen.is-done .plusnet-screen__page,
:root[data-motion="paused"] .plusnet-screen__page {
  animation: none !important;
}

/* In the SVG's units (the artwork's pixels ÷ --k) */
@keyframes plusnet-scroll {
  0% { transform: translateY(0); animation-timing-function: cubic-bezier(0.65, 0, 0.35, 1); }
  40%,
  63% { transform: translateY(calc((560px - var(--sy)) / var(--k) - 40.34px - 546.29px)); animation-timing-function: cubic-bezier(0.65, 0, 0.35, 1); }
  100% { transform: translateY(0); }
}

/* The stage is always the XL tile, so its animated parts take their XL positions at every
   width, whatever the breakpoint rules above say */
.work-art .work-stage .brands {
  --art-w: 1200px; --fx: 640px; --fy: 140px; --fw: 280px; --fh: 280px;
  --box-x: 20px; --box-y: 20px; --box-w: 240px; --box-h: 240px;
  --logo: 88px; --bt-x: 96px; --bt-y: 50px; --ee-x: 46px; --ee-y: 138px; --pn-x: 145px; --pn-y: 138px;
  --bt-start: 1.6; --dark-shift: 0px;
}
.work-art .work-stage .brands__rect--square { display: inline; }
.work-art .work-stage .brands__rect--wide { display: none; }
.work-art .work-stage .debug-split { --half-px: 0px; }
.work-art .work-stage .plusnet-screen { --art-w: 588px; --sx: 157.28px; --sy: 208.26px; --k: 1; }

/* Dot jump between sections (app.js): under the top bar */
.dot-jump { position: fixed; inset: 0; z-index: 9; width: 100%; height: 100%; pointer-events: none; }
.dot-jump[hidden] { display: none; }


/* Tablet (M, L): the same layout with narrower columns */
@media (max-width: 1279px) {
  .about { grid-template-columns: minmax(0, 5fr) minmax(0, 6fr); }
  .pull { font-size: 24px; line-height: 28px; }
}

/* M: one quote per row, the pull line beside its paragraph */
@media (min-width: 768px) and (max-width: 1023px) {
  .quotes { grid-template-columns: 1fr; gap: 48px; }
  .quotes figure { grid-template-columns: minmax(0, 5fr) minmax(0, 6fr); column-gap: 40px; }
  .pull { grid-row: 1 / 3; }
}

/* Mobile (XS + S, under 768px) */
@media (max-width: 767px) {
  :root { --gutter: 16px; }
  .top-links, .top-end { display: none; }
  .top { grid-template-columns: auto 1fr; }
  .menu-btn { display: inline-flex; justify-self: end; }
  .top-end { gap: 14px; }
  .work { padding-bottom: 120px; }
  .work-list { gap: 56px; }
  .work-art { border-radius: 12px; }
  /* The line above, as on desktop; then the tag, the title and stat on one baseline, then the description */
  .work-meta { grid-template-columns: 1fr auto; align-items: baseline; padding: 16px 0; }
  .work-tag, .work-desc { grid-column: 1 / -1; }
  .work-stat { grid-row: 2; grid-column: 2; align-self: start; gap: 4px; } /* the number and label sit at the top of the title row */
  .work-stat__num { font-size: 28px; line-height: 28px; }
  .statement { padding: 120px var(--gutter); }
  .about { grid-template-columns: 1fr; row-gap: 48px; padding-top: 120px; padding-bottom: 120px; }
  .dots-portrait { order: -1; }
  .dots-canvas { height: auto; min-height: 0; aspect-ratio: 695 / 764; }
  .about-title { font-size: 44px; line-height: 44px; }
  .career li { grid-template-columns: 1fr 40px; row-gap: 0; } /* the dates sit straight under the role (Figma Narrow) */
  .career-when { grid-column: 1; grid-row: 2; }
  .career-logo { grid-column: 2; grid-row: 1 / 3; }
  .words { gap: 48px; padding-top: 120px; padding-bottom: 120px; }
  .quotes-wrap { gap: 40px; }
  .quotes { grid-template-columns: 1fr; gap: 40px; }
  .pull { font-size: 22px; line-height: 26px; }
  .contact { padding-top: 120px; gap: 48px; }
  .video-stage { width: calc(100vw - 2 * var(--gutter)); } /* the screen's width, not the wider people row's */
  /* The video is taller than the people here, so its room isn't kept while it's shut: the
     slot is the people's height and eases open when the video comes in (app.js) */
  .people-wrap:not(.is-video) .video-stage { position: absolute; top: 0; }
  /* Footer, centred: the two lines in Plex Sans, then the © and the place in mono labels
     (like "My career"), then the theme and pause buttons */
  .foot { grid-template-columns: 1fr; justify-items: center; gap: 4px; padding: 32px var(--gutter) 40px; }
  .foot p:nth-child(n) { text-align: center; }
  .foot .foot-date { order: 1; margin-top: 20px; }
  .foot p:nth-child(1) { order: 3; margin-top: 20px; } /* the copyright closes the page, under the controls */
  .foot-date__sep { display: none; } /* the place takes its own line */
  .foot-date__place { display: block; }
  .foot-controls { order: 2; justify-self: center; margin-top: 20px; }
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after { transition-duration: .01ms !important; }
  .hero-intro, .hero-name, .nf-art, .nf-text { opacity: 1; translate: 0; }
}

/* Links highlight on hover (and keyboard focus) like the hero's "users": an ink fill
   sweeps across from the left, the way selecting text does, the words turn to the page
   colour, and an underline draws across with the fill, in step. The underline is a 1px
   background line --ul up from the bottom, where the text's own underline would sit.
   With Reduce motion on it simply appears. */
.top a:not(.top-home), .work-title, .vctl, .video-desc summary, .career-co, .quotes-more, .menu-btn, .menu-links a {
  --ul: 4px;
  background:
    linear-gradient(var(--ink-inverse), var(--ink-inverse)) left 0 bottom var(--ul) / 0 1px no-repeat,
    linear-gradient(var(--ink), var(--ink)) left / 0 100% no-repeat;
  transition: background-size .35s var(--ease), color .2s var(--ease);
}
.work-title { --ul: 1px; } /* 14px text on 20px lines sits lower in its box */
.career-co { --ul: 2px; }
/* A fine ↗ after each company name: it opens their site in a new tab (Plex has no ↗, so it's drawn) */
.career-co { display: inline-flex; align-items: center; gap: 3px; }
.career-co__arrow { width: 13px; height: 13px; }
:is(.top a:not(.top-home), .vctl, .video-desc summary, .career-co, .quotes-more, .menu-btn, .menu-links a):focus-visible,
.work-link:focus-visible .work-title,
.career li.is-active .career-co { /* the whole row: hovered, tapped or focused (app.js) */
  background-size: 100% 1px, 100% 100%;
  color: var(--ink-inverse);
}
/* Hover only where there's a real pointer: on phones and tablets a tap would leave it stuck on */
@media (hover: hover) {
  :is(.top a:not(.top-home), .vctl, .video-desc summary, .career-co, .quotes-more, .menu-btn, .menu-links a):hover,
  .work-link:hover .work-title {
    background-size: 100% 1px, 100% 100%;
    color: var(--ink-inverse);
  }
}
@media (prefers-reduced-motion: reduce) {
  .top a:not(.top-home), .work-title, .vctl, .video-desc summary, .career-co, .quotes-more, .menu-btn, .menu-links a { transition: none; }
  .menu-btn__icon { transition: none; }
}

/* ---------- Code fun (on trial, js/fun.js) ----------
   The 404's terminal voice in three quiet places (the fifth is a note in the browser
   console). Plex Mono 12/20 at 4% (Figma Type/label/meta), muted, ink where it's typed.
   Each line holds its room from the start, so nothing moves while it types. */
.foot-date.is-waiting { visibility: hidden; }
.foot-date.is-typing :is(.key:not(.is-typed), [data-type="out"]:not(.is-typed)) { visibility: hidden; }
:is(.foot-date, .copy-reply) .caret::after { top: -11px; height: 13px; }

/* 3. Hero: the diff of the edit, under the line, for a moment */
.hero-intro { position: relative; }
.hero-diff {
  position: absolute; left: 50%; top: calc(100% - 69px); translate: -50% 4px; /* 12px under the words (the intro has 81px below them) */
  display: flex; gap: 16px; white-space: nowrap;
  font: 500 12px/20px var(--mono); letter-spacing: .04em;
  opacity: 0; transition: opacity .6s var(--ease), translate .6s var(--ease);
}
.hero-diff.is-shown { opacity: 1; translate: -50% 0; }
.hero-diff__del { color: var(--muted); }
.hero-diff__add { color: var(--ink); }

/* 5. Email: "$ copied" right under whatever was pressed (the email button, the Message
   link, or the Menu button on phones), the same in each place; fun.js lines it up */
.contact-actions { position: relative; }
.copy-reply {
  position: absolute; left: 50%; top: calc(100% + 6px); translate: -50% 4px;
  padding: 0 8px; border-radius: 999px;
  background: color-mix(in srgb, var(--bg) 90%, transparent); backdrop-filter: blur(10px);
  font: 500 12px/20px var(--mono); letter-spacing: .04em; color: var(--muted); white-space: nowrap;
  pointer-events: none;
  opacity: 0; transition: opacity .3s var(--ease), translate .3s var(--ease);
}
.copy-reply.is-shown { opacity: 1; translate: -50% 0; }

/* ---------- On scroll, every page (site.js) ----------
   The big serif headings rise into place word by word, a line at a time, each word coming
   up from behind its own edge, like type set line by line. Text, lists and cards fade up a
   little, one after another. Only what starts below the screen plays, once; each keeps its
   room from the start, so nothing around it moves. Reduce motion or paused: all simply there. */
.reveal { opacity: 0; transform: translateY(14px); transition: opacity .9s var(--ease) var(--reveal-delay, 0s), transform .9s var(--ease) var(--reveal-delay, 0s); }
.reveal.is-in { opacity: 1; transform: none; }
.rise-word { display: inline-block; clip-path: inset(-.5em -.3em -.3em -.3em); }
.rise-word__in {
  display: inline-block; transform: translateY(calc(100% + .35em));
  transition: transform 1s cubic-bezier(.2, .75, .25, 1) var(--line-delay, 0s);
}
.is-rising .rise-word__in { transform: none; }

/* ---------- Changing page (site.js) ----------
   Arriving from another page of the site, the page starts covered in its own colour under
   the top bar (data-arriving, set in <head> before it paints), and the dot jump's specks
   scatter away from it. If the script doesn't run, the cover lifts by itself. */
:root[data-arriving]::after {
  content: ""; position: fixed; inset: 0; z-index: 9; background: var(--bg);
  animation: arrive-lift 0s 2s forwards;
}
@keyframes arrive-lift { to { visibility: hidden; } }

@media print {
  .reveal { opacity: 1 !important; transform: none !important; }
  .rise-word__in { transform: none !important; }
  .dot-jump, :root[data-arriving]::after { display: none !important; }
}
