/* ==========================================================================
   Home page
   Sections stack: each one sticks when you reach its end, and the next one
   slides up over it (home.js sets each section's sticky offset).
     0. Hello intro
     1. The page as a document
     2. Hero
     3. About me
     4. Case studies
     5. Artwork and lightbox
   ========================================================================== */

.dot {
  display: inline-block;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--accent);
}

.label {
  display: flex;
  align-items: center;
  gap: 12px;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text);
}

.muted {
  color: var(--text);
}

/* 0. Hello intro ------------------------------------------------------------
   Pure CSS, after Apple's "hello". Timeline:
     0.2s - 2.2s  "hello" writes itself as one continuous line (its stroke is
                  revealed with stroke-dashoffset; pathLength="1" makes the
                  line one unit long), at an even, unhurried pace
     2.8s - 3.4s  the screen fades away with "hello" on it, then the name's
                  letters rise in (--intro on .hero delays it)
   The overlay is fixed on top of everything and never takes
   clicks once it has faded. It plays on a fresh visit or a refresh only, not
   when you return to Home from another page of the site, and is skipped
   entirely for reduced motion. */
.hello {
  --hello-w: min(560px, 72vw); /* width of the "hello" drawing */
  position: fixed;
  inset: 0;
  z-index: 100;
  display: grid;
  place-items: center;
  background: var(--bg);
  animation: hello-out 0.6s ease 2.8s forwards;
}
.hello__word {
  width: var(--hello-w);
  overflow: visible;
}
.hello__word path {
  fill: none;
  stroke: var(--ink);
  stroke-width: 11;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-dasharray: 1;
  stroke-dashoffset: 1;
  animation: hello-write 2s cubic-bezier(0.45, 0.05, 0.55, 0.95) 0.2s forwards;
}
@keyframes hello-write {
  to {
    stroke-dashoffset: 0;
  }
}
@keyframes hello-out {
  to {
    opacity: 0;
    visibility: hidden;
  }
}
/* the name's letters wait for the greeting to finish */
.hero {
  --intro: 3.5s;
}
/* while the intro plays (see the script in index.html) the page cannot
   scroll; the scrollbar's space is kept so nothing shifts when it unlocks */
html.hello-lock,
html.hello-lock body {
  overflow: hidden;
}
html.hello-lock {
  scrollbar-gutter: stable;
}
/* coming back from another page of the site (see the script in index.html),
   or reduced motion: no intro, the name appears straight away */
.no-hello .hello {
  display: none;
}
.no-hello .hero {
  --intro: 0s;
}
@media (prefers-reduced-motion: reduce) {
  .hello {
    display: none;
  }
  .hero {
    --intro: 0s;
  }
}

/* 1. The page as a document ----------------------------------------------
   Laid out like the case study pages: the top bar (site.css), a plain outline of
   the page down the left with a mark on the section you are reading
   (home.js), and the sections beside it, one after another like the stages
   of a case study. Each section opens with the case study's stage header:
   a thin rule carrying its number, a large faint outlined ghost of its
   name, and the title overlapping the ghost's lower part. Below 1100px the
   outline goes and the page takes the full width. */
:root {
  --gap-stage: clamp(72px, 11vh, 120px);
}
.cp-outline a:focus-visible {
  outline: 3px solid var(--ink);
  outline-offset: 2px;
  border-radius: 4px;
}

/* The tip under the pause-motion button: a small dark note with an arrow
   pointing up at the button. It is fixed just under the top bar. home.js
   lines it up from the button: the note's right edge sits just past the
   button's (--tip-left), and the arrow sits under the button's centre
   (--arrow-x), the note keeping 16px from the screen edges. It rises in and
   the arrow nudges toward the button twice. */
.motion-tip {
  position: fixed;
  z-index: 45;
  top: calc(var(--bar-h) + 10px);
  left: var(--tip-left, 16px);
  display: flex;
  flex-direction: column;
  gap: 10px;
  width: min(280px, calc(100vw - 32px));
  padding: 14px 16px;
  border-radius: 12px;
  background: var(--ink);
  color: var(--bg);
  box-shadow: 0 12px 32px rgb(0 0 0 / 0.22);
  font-size: 0.875rem;
  line-height: 1.45;
  animation: tip-in 0.4s cubic-bezier(0.2, 0.7, 0.2, 1) both;
}
.motion-tip[hidden] {
  display: none;
}
/* the arrow: its own small square, turned 45deg, half above the note's top
   edge; it is the note's first item and its margin moves it under the
   button */
.motion-tip__arrow {
  align-self: flex-start;
  width: 16px;
  height: 16px;
  margin: -23px 0 -3px calc(var(--arrow-x, 24px) - 24px);
  border-radius: 2px;
  background: var(--ink);
  rotate: 45deg;
  animation: tip-nudge 0.9s 0.5s ease-in-out 2;
}
.motion-tip__text {
  margin: 0;
  font-size: 0.9375rem;
}
.motion-tip__text b {
  font-weight: 600;
}
.motion-tip__ok {
  align-self: flex-end;
  min-height: 32px;
  padding: 0 14px;
  border: 0;
  border-radius: 999px;
  background: var(--bg);
  color: var(--ink);
  font: inherit;
  font-size: 0.875rem;
  font-weight: 600;
  cursor: pointer;
}
.motion-tip__ok:focus-visible {
  outline: 3px solid var(--ink);
  outline-offset: 2px;
}
.motion-tip__ok:hover {
  background: color-mix(in srgb, var(--bg) 82%, var(--ink));
}
/* the timer: a thin bar along the bottom edge that drains from right to
   left over 8s; when it ends home.js closes the tip. It holds while the
   pointer or keyboard focus is on the tip, so it never closes mid-read. */
.motion-tip__timer {
  order: 9;
  height: 3px;
  margin: 0 0 -4px;
  border-radius: 2px;
  background: color-mix(in srgb, var(--bg) 55%, transparent);
  transform-origin: left;
  animation: tip-timer 8s linear forwards;
}
.motion-tip:is(:hover, :focus-within) .motion-tip__timer {
  animation-play-state: paused;
}
@keyframes tip-timer {
  to {
    transform: scaleX(0);
  }
}
.motion-tip.is-leaving {
  animation: tip-out 0.25s ease both;
}
@keyframes tip-in {
  from {
    opacity: 0;
    translate: 0 8px;
  }
}
@keyframes tip-out {
  to {
    opacity: 0;
    translate: 0 -4px;
  }
}
@keyframes tip-nudge {
  50% {
    translate: 0 -4px;
  }
}

/* Pause motion: the button in the top bar stops everything that moves by
   itself (WCAG 2.2.2): the skill rings, the letter wave, the falling leaves
   (home.js pauses those), the plants, the badge's sway and the blinking
   caret. Motion that follows your scrolling keeps working. The button shows
   pause while things move and play once they are paused; aria-pressed
   tells screen readers which. */
.motion-toggle {
  color: var(--ink);
}
.motion-toggle .icon-play,
.motion-paused .motion-toggle .icon-pause {
  display: none;
}
.motion-paused .motion-toggle .icon-play {
  display: block;
}
.motion-toggle:focus-visible {
  outline: 3px solid var(--ink);
  outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
  /* nothing moves by itself already */
  .motion-toggle {
    display: none;
  }
}
:root.motion-paused :is(.skills3d__tilt, .skills3d__spin, .plant--succulent, .plant--pothos, .plant__sway, .flutter, .drift, .hover, .spot__hint, .tagline__caret, .badge__swing) {
  animation-play-state: paused !important;
}
/* the letters keep their rise on load but drop the wave */
:root.motion-paused .hero__ch {
  animation-name: hero-rise, none !important;
}

.doc {
  position: relative;
  z-index: 1;
  display: grid;
  grid-template-columns: minmax(180px, 220px) minmax(0, 1fr);
  column-gap: clamp(32px, 4vw, 64px);
  padding-inline: max(var(--gx), calc((100% - var(--max)) / 2 + var(--gx)));
  background: var(--paper) 0 0 / 240px auto, var(--bg);
}
.doc-side {
  grid-column: 1;
  grid-row: 1;
  position: sticky;
  top: var(--bar-h);
  display: flex;
  flex-direction: column;
  height: calc(100vh - var(--bar-h));
  padding-block: 20px;
  overflow-y: auto;
  scrollbar-width: none;
}
.doc-main {
  grid-column: 2;
  grid-row: 1;
  min-width: 0;
}
/* the sections' own side margins come from the document now */
.doc-main .container {
  max-width: none;
  padding-inline: 0;
}
.sections {
  overflow-x: clip;
}

.cp-outline {
  margin-top: clamp(40px, 10vh, 120px);
}
.cp-outline > ol {
  padding-left: 14px;
}
.cp-outline ol {
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.cp-outline ol ol {
  margin: 2px 0 6px 12px;
}
.cp-outline a {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 3px 8px 3px 11px;
  border-radius: 0 6px 6px 0;
  /* the wash and the bar that mark the section you are reading, as on the
     case study pages; the bar is the link's left border, so nothing is
     positioned */
  border-left: 3px solid transparent;
  background: linear-gradient(90deg in oklch,
      color-mix(in oklch, var(--accent) 20%, transparent),
      color-mix(in oklch, var(--accent) 8%, transparent) 55%,
      transparent)
    no-repeat 0 0 / 0% 100%;
  font-size: 0.875rem;
  line-height: 1.35;
  color: var(--muted);
  text-decoration: none;
  transition: color 0.2s ease, background-size 0.4s ease, border-color 0.3s ease;
}
.cp-outline ol ol a {
  font-size: 0.8125rem;
}
/* each part of the page is numbered, small, the same numbers its stage
   header carries; screen readers skip the number */
.cp-outline > ol {
  counter-reset: part;
}
.cp-outline > ol > li {
  counter-increment: part;
}
.cp-outline > ol > li > a::before {
  content: counter(part) "." / "";
  min-width: 1.4em;
  margin-right: -2px;
  font-size: 0.8125rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  opacity: 0.7;
}
.cp-outline ol ol {
  margin-left: calc(12px + 1.4em);
}
.cp-outline a:hover {
  color: var(--ink);
}
/* the section you are reading: the bar, the wash, orange and bold; the
   others fade back to just above the 4.5:1 contrast text needs */
.cp-outline a:not([aria-current]) {
  color: var(--outline-dim);
}
.cp-outline a:not([aria-current]):hover {
  color: var(--ink);
}
.cp-outline a[aria-current] {
  border-left-color: var(--accent);
  background-size: 100% 100%;
  color: var(--accent-text);
  font-weight: 600;
}
@media (prefers-reduced-motion: reduce) {
  .cp-outline a {
    transition: none;
  }
}

/* every section after the hero opens like a case study stage */
.sections > section:not(.hero) {
  padding-top: var(--gap-stage);
}
.doc-head {
  display: flex;
  flex-direction: column;
  margin-bottom: clamp(24px, 4vh, 40px);
}
.doc-head::before {
  content: attr(data-num) / "";
  display: block;
  min-height: 1em;
  margin-bottom: 16px;
  background: linear-gradient(var(--line), var(--line)) no-repeat right center / calc(100% - 2.8em) 1px;
  font-family: var(--body-font);
  font-size: 0.8125rem;
  font-weight: 600;
  line-height: 1;
  letter-spacing: 0.12em;
  font-variant-numeric: tabular-nums;
  color: var(--accent-text);
}
.doc-head::after {
  content: attr(data-ghost) / "";
  order: -1;
  display: block;
  max-width: 100%;
  overflow: hidden;
  margin-bottom: -0.5em;
  font-family: var(--heading-font);
  font-size: clamp(3.5rem, 2rem + 5vw, 6.5rem);
  font-weight: 700;
  line-height: 0.9;
  letter-spacing: 0.01em;
  text-transform: uppercase;
  white-space: nowrap;
  color: transparent;
  -webkit-text-stroke: 1px var(--ink);
  opacity: 0.14;
  pointer-events: none;
  user-select: none;
}
.doc-head::before {
  order: -2;
}
@supports not (-webkit-text-stroke: 1px black) {
  .doc-head::after {
    color: var(--ink);
    opacity: 0.06;
  }
}
.doc-head__title {
  position: relative;
  z-index: 1;
  padding-bottom: 6px;
  font-size: clamp(1.75rem, 1.4rem + 1.1vw, 2.375rem);
  line-height: 1.05;
  letter-spacing: -0.005em;
}

@media (max-width: 1100px) {
  .doc {
    grid-template-columns: minmax(0, 1fr);
  }
  .doc-side {
    display: none;
  }
  .doc-main {
    grid-column: 1;
  }
}

/* 2. Hero -------------------------------------------------------------------- */
/* The hero holds for 30vh of scrolling (130vh tall, its content sticky
   under the top bar): the name swaps between 5vh and 20vh and rests a
   moment as "UX Developer." before About me comes up. */
.hero {
  height: 130vh;
}
.hero__sticky {
  position: sticky;
  top: var(--bar-h);
  display: grid;
  grid-template-rows: 1fr auto;
  height: calc(100vh - var(--bar-h));
}
.hero__words {
  display: grid;
  place-items: center;
  text-align: center;
}
/* Skills orbiting the name. Two rings of letters turn round the name and
   swing about like a gyroscope: each ring's tilted axis sweeps round the
   vertical (precession) and rocks a little (nutation), the two in opposite
   directions so they keep crossing. The rings are seen from a little above,
   so they never turn edge-on, and every letter is turned back to face you
   and stand upright wherever it is on its ring, so the words read at any
   moment. The ring's angles are registered custom properties: they turn the
   ring, and each letter undoes the same angles in reverse order. The same
   angles give each letter its depth (--z, -1 at the back to 1 at the
   front), which fades the letters going round the back.
   Each ring is drawn twice, in the same grid cell as the name: the back
   layer sits under the name and shows the whole ring faintly; the front
   layer sits over the name and shows only the letters on the near side.
   Both layers run the same animations from the same moment, so they stay
   in step. Nothing is positioned: every layer shares the cell. */
@property --ring-c {
  syntax: "<angle>";
  inherits: true;
  initial-value: 0deg;
}
@property --ring-x {
  syntax: "<angle>";
  inherits: true;
  initial-value: 0deg;
}
@property --ring-y {
  syntax: "<angle>";
  inherits: true;
  initial-value: 0deg;
}
@property --ring-z {
  syntax: "<angle>";
  inherits: true;
  initial-value: 0deg;
}
@property --ring-spin {
  syntax: "<angle>";
  inherits: true;
  initial-value: 0deg;
}
.skills3d {
  --r: min(34vw, 400px, 44vh); /* the ring's radius */
  grid-area: 1 / 1;
  display: grid;
  place-items: center;
  width: calc(var(--r) * 2);
  perspective: calc(var(--r) * 3.5);
  pointer-events: none;
  user-select: none;
  animation: skills-in 1.4s calc(var(--intro, 0s) + 1s) both;
}
/* without the outline the hero has the full width: wider rings */
@media (max-width: 1100px) {
  .skills3d {
    --r: min(43vw, 400px, 44vh);
  }
}
.hero__name {
  grid-area: 1 / 1;
  z-index: 1;
}
.skills3d--front {
  z-index: 2;
}
.skills3d__tilt {
  --ring-c: -22deg; /* seen from a little above */
  --ring-x: -9deg; /* the ring's tilt: less than the view from above, so the
                  ring never turns edge-on to you */
  --ring-z: -6deg;
  grid-area: 1 / 1;
  display: grid;
  transform-style: preserve-3d;
  transform: rotateX(var(--ring-c)) rotateZ(var(--ring-z)) rotateY(var(--ring-y)) rotateX(var(--ring-x));
  animation: gyro-a 20s linear infinite;
}
.skills3d__tilt--b {
  --ring-x: 8deg;
  --ring-z: 8deg;
  --ring-y: 120deg;
  /* a little smaller, so the rings cross instead of sitting on each other */
  transform: rotateX(var(--ring-c)) rotateZ(var(--ring-z)) rotateY(var(--ring-y)) scale3d(0.8, 0.8, 0.8) rotateX(var(--ring-x));
  animation-name: gyro-b;
  animation-duration: 26s;
}
.skills3d__spin {
  display: grid;
  transform-style: preserve-3d;
  transform: rotateY(var(--ring-spin));
  font-family: var(--body-font);
  font-size: clamp(14px, 0.55rem + 0.7vw, 20px);
  font-weight: 700;
  line-height: 1;
  letter-spacing: 0.02em;
  color: var(--ink);
  animation: skills-spin 44s linear infinite;
}
/* the second ring in the body grey: the hero keeps its orange for the
   status dot and the full stop after "Developer" */
.skills3d__tilt--b .skills3d__spin {
  animation-duration: 58s;
  animation-direction: reverse;
  color: var(--text);
}
.skills3d__ch {
  grid-area: 1 / 1;
  display: grid;
  transform-style: preserve-3d;
  transform: rotateY(calc(var(--i) * 360deg / var(--n))) translateZ(var(--r));
}
.skills3d__ch::before {
  /* where the letter is round the ring, and its depth after every turn */
  --th: calc(var(--ring-spin) + var(--i) * 360deg / var(--n));
  --x1: sin(var(--th));
  --y1: calc(-1 * cos(var(--th)) * sin(var(--ring-x)));
  --z1: calc(cos(var(--th)) * cos(var(--ring-x)));
  --x2: calc(var(--x1) * cos(var(--ring-y)) + var(--z1) * sin(var(--ring-y)));
  --z2: calc(var(--z1) * cos(var(--ring-y)) - var(--x1) * sin(var(--ring-y)));
  --z: calc(sin(var(--ring-c)) * (var(--x2) * sin(var(--ring-z)) + var(--y1) * cos(var(--ring-z))) + cos(var(--ring-c)) * var(--z2));
  content: attr(data-c);
  /* undo every turn above it, last first, so it stands upright facing you */
  transform: rotateY(calc(var(--i) * -360deg / var(--n))) rotateY(calc(-1 * var(--ring-spin)))
    rotateX(calc(-1 * var(--ring-x))) rotateY(calc(-1 * var(--ring-y))) rotateZ(calc(-1 * var(--ring-z))) rotateX(calc(-1 * var(--ring-c)));
}
.skills3d--back .skills3d__ch::before {
  opacity: clamp(0.12, calc(0.3 + var(--z) * 0.2), 0.3);
}
.skills3d--front .skills3d__ch::before {
  opacity: clamp(0, calc(0.4 + var(--z) * 4), 1);
}
@keyframes gyro-a {
  0% {
    --ring-y: 0deg;
    --ring-x: -9deg;
    --ring-z: -6deg;
  }
  25% {
    --ring-y: 90deg;
    --ring-x: -13deg;
    --ring-z: 2deg;
  }
  50% {
    --ring-y: 180deg;
    --ring-x: -9deg;
    --ring-z: 6deg;
  }
  75% {
    --ring-y: 270deg;
    --ring-x: -13deg;
    --ring-z: -2deg;
  }
  100% {
    --ring-y: 360deg;
    --ring-x: -9deg;
    --ring-z: -6deg;
  }
}
@keyframes gyro-b {
  0% {
    --ring-y: 120deg;
    --ring-x: 8deg;
    --ring-z: 8deg;
  }
  50% {
    --ring-y: -60deg;
    --ring-x: 12deg;
    --ring-z: -4deg;
  }
  100% {
    --ring-y: -240deg;
    --ring-x: 8deg;
    --ring-z: 8deg;
  }
}
@keyframes skills-spin {
  to {
    --ring-spin: -360deg;
  }
}
@keyframes skills-in {
  from {
    opacity: 0;
  }
}
@media (prefers-reduced-motion: reduce) {
  .skills3d,
  .skills3d__tilt,
  .skills3d__spin {
    animation: none;
  }
}

/* Hero name, built like the Loire hero. Set in Inter, every word the same
   weight. Each line is a strict window (overflow hidden) holding two words
   stacked with no gap: the name word, and the role word right under it. On
   scroll both words move up exactly one word height together, so the role
   word replaces the name word inside the same window and is never seen
   outside it. The words are stacked tight (Loire's 0.85 line), so the role
   word sits directly behind the name and the move reads as one push. The
   window stops just above the waiting word's tallest letters. The move follows the scroll position on an
   ease-out curve (see "Scroll-driven motion" below); browsers without
   scroll-driven animations get the same move as a transition when home.js
   adds .is-swapped. */
.hero__name {
  --word: 0.85em; /* one word's line box, as tight as Loire's */
  --slot: 0.87em; /* the window: stops just above the waiting word */
  display: flex;
  flex-direction: column;
  align-items: center;
  margin: 0;
  font-family: var(--body-font);
  font-size: clamp(3rem, 10vw, 8.5rem);
  font-weight: 600;
  line-height: 0.85;
  letter-spacing: -0.035em;
}
.hero__line {
  display: grid;
  grid-template-rows: var(--slot);
  overflow: hidden;
  margin-bottom: -0.03em;
}
.hero__reel {
  grid-area: 1 / 1;
}
.hero__reel {
  align-self: start;
  display: flex;
  flex-direction: column;
  align-items: center;
  translate: 0 0;
  transition: translate 0.7s cubic-bezier(0.33, 1, 0.68, 1);
}
.hero.is-swapped .hero__reel {
  translate: 0 calc(-1 * var(--word));
}
.hero__word {
  display: block;
  height: var(--word);
}
.hero__word--role i {
  font-style: normal;
  color: var(--accent);
}

/* Letter animation, pure CSS (home.js only wraps each letter in a span and
   numbers it). On load the letters rise one after another into their line's
   window. Every few seconds after that a wave runs along the name: each
   letter leaves through the top of the window and comes back up from below,
   a little after the one before it. Hovering the name sends the wave at once.
   The window clips the letters, so a letter is never seen outside its line.
   The same happens to the role words once they have replaced the name. */
.hero__ch {
  display: inline-block;
  animation:
    hero-rise 0.9s calc(var(--intro, 0s) + var(--wipe-delay, 0.2s) + var(--i) * 0.05s) cubic-bezier(0.22, 1, 0.36, 1) both,
    hero-roll 7s calc(var(--intro, 0s) + 3s + var(--wipe-delay, 0.2s) + var(--i) * 0.045s) infinite;
}
.hero__line:nth-child(2) {
  --wipe-delay: 0.32s;
}
@media (hover: hover) {
  .hero__name:hover .hero__ch {
    animation:
      hero-rise 0.9s calc(var(--intro, 0s) + var(--wipe-delay, 0.2s) + var(--i) * 0.05s) cubic-bezier(0.22, 1, 0.36, 1) both,
      hero-roll-now 0.7s calc(var(--i) * 0.045s) both;
  }
}
/* only the word showing in the window waves: the hidden one would pass
   through the window on its way round */
.hero:not(.is-swapped) .hero__word--role .hero__ch,
.hero.is-swapped .hero__word:not(.hero__word--role) .hero__ch {
  animation-name: hero-rise, none;
}
@keyframes hero-rise {
  from {
    transform: translateY(var(--slot));
  }
  to {
    transform: none;
  }
}
/* the wave takes the first tenth of each 7s cycle; the letters rest after */
@keyframes hero-roll {
  0% {
    translate: 0 0;
    animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45);
  }
  4.99% {
    translate: 0 calc(-1 * var(--slot));
  }
  5% {
    translate: 0 var(--slot);
    animation-timing-function: cubic-bezier(0, 0.55, 0.45, 1);
  }
  10%,
  100% {
    translate: 0 0;
  }
}
@keyframes hero-roll-now {
  0% {
    translate: 0 0;
    animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45);
  }
  49.99% {
    translate: 0 calc(-1 * var(--slot));
  }
  50% {
    translate: 0 var(--slot);
    animation-timing-function: cubic-bezier(0, 0.55, 0.45, 1);
  }
  100% {
    translate: 0 0;
  }
}
@media (prefers-reduced-motion: reduce) {
  .hero__ch {
    animation: none !important;
  }
}

.hero__bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 12px 24px;
  padding-bottom: 28px;
  color: var(--text);
  opacity: calc(1 - var(--hero, 0) * 5);
}
.hero__bar p:first-child {
  display: flex;
  align-items: center;
  gap: 12px;
  color: var(--ink);
}
/* Tagline: "I design and code." with a little personality. "design" is
   handwritten (Kalam) and tilted;
   "code" is monospaced with a blinking text cursor after it. */
.tagline__hand {
  display: inline-block;
  margin-inline: 0.1em;
  font-family: "Kalam", var(--body-font);
  font-size: 1.4em;
  line-height: 1;
  color: var(--ink);
  rotate: -4deg;
}
.tagline__code {
  margin-inline: 0.15em 0;
  padding: 0.05em 0.35em;
  border-radius: 6px;
  background: color-mix(in srgb, var(--ink) 8%, transparent);
  font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
  font-size: 0.95em;
}
.tagline__caret {
  display: inline-block;
  width: 0.55em;
  height: 1.05em;
  margin-left: 0.15em;
  vertical-align: -0.15em;
  background: var(--ink);
  animation: caret-blink 1.1s steps(1) infinite;
}
@keyframes caret-blink {
  50% {
    opacity: 0;
  }
}
@media (prefers-reduced-motion: reduce) {
  .tagline__caret {
    animation: none;
  }
}
.hero__links {
  display: flex;
  gap: 24px;
}
.hero__links a {
  color: var(--text);
  text-decoration: none;
}
.hero__links a:hover {
  color: var(--ink);
}

/* 3. About me ---------------------------------------------------------------- */
.intro {
  padding-bottom: clamp(32px, 6vh, 64px);
}
.intro__inner {
  display: flex;
  flex-direction: column;
}
/* Same language as Case studies: a huge title (here on the left, mirroring
   Case studies on the right) on the same 12-column grid, with the statement
   set large beside it. */
.intro__top {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  column-gap: 24px;
  row-gap: 32px;
  align-items: end;
}
.statement {
  grid-column: 6 / -1;
  grid-row: 1;
  align-self: start;
  max-width: 26ch;
  font-family: var(--heading-font);
  font-size: var(--fs-statement);
  font-weight: 600;
  line-height: 1.2;
  letter-spacing: -0.02em;
  color: var(--ink);
}
@media (max-width: 860px) {
  .statement {
    grid-column: 1 / -1;
  }
}
.fill-word {
  background: linear-gradient(90deg, var(--ink) var(--f, 0%), color-mix(in srgb, var(--ink) 26%, transparent) var(--f, 0%));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

/* the ID badge (styles in site.css) hangs under the About me title */
.intro .badge {
  grid-column: 1 / span 4;
  grid-row: 1;
  justify-self: center;
  align-self: start;
  margin-top: -16px;
}
@media (max-width: 860px) {
  .intro .badge {
    grid-column: 1 / -1;
    grid-row: auto;
    margin-top: 16px;
  }
}

/* 4. Case studies ------------------------------------------------------------
   Each project sits on its own floating wall shelf, on the screen it was
   made for: Luna on a monitor, KU on a laptop, Asian's on a phone. The
   shelves step down from left to right. Everything is sized in container
   units (cqw) of .shelves, so the whole wall scales with the page and the
   steps stay in proportion. Below 860px the shelves stack.
   Falling leaves, petals, fireflies or snow (home.js) play in .falling over
   the whole section; the season switch sets data-season on .work, which the
   plants read here (spring flowers, winter snow). */
.work {
  --wood: #c9a77c;
  --wood-top: #dcc19b;
  --wood-edge: #b18f64;
  --board-shadow: 0 14px 22px rgb(60 40 10 / 0.18);
  --laptop: #e4e4de;
  --laptop-edge: #d0d0c9;
  --laptop-dot: #c9c9c2;
  --laptop-base: linear-gradient(#d4d4cd, #b9b9b2);
  --stand: #c3c3bc;
  --cord: #a8a8a0;
  --device-shadow: 0 24px 40px rgb(0 0 0 / 0.22);
  --bezel-edge: transparent;
  position: relative;
  padding-bottom: clamp(56px, 8vh, 88px);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .work {
    --wood: #6b4f36;
    --wood-top: #8a6a4b;
    --wood-edge: #4e3826;
    --board-shadow: 0 14px 22px rgb(0 0 0 / 0.55);
    --laptop: #26262b;
    --laptop-edge: #34343a;
    --laptop-dot: #3e3e45;
    --laptop-base: linear-gradient(#3a3a40, #2a2a2f);
    --stand: #3a3a40;
    --cord: #5a5a62;
    --device-shadow: 0 24px 40px rgb(0 0 0 / 0.6);
    --bezel-edge: rgb(255 255 255 / 0.14);
  }
}
:root[data-theme="dark"] .work {
  --wood: #6b4f36;
  --wood-top: #8a6a4b;
  --wood-edge: #4e3826;
  --board-shadow: 0 14px 22px rgb(0 0 0 / 0.55);
  --laptop: #26262b;
  --laptop-edge: #34343a;
  --laptop-dot: #3e3e45;
  --laptop-base: linear-gradient(#3a3a40, #2a2a2f);
  --stand: #3a3a40;
  --cord: #5a5a62;
  --device-shadow: 0 24px 40px rgb(0 0 0 / 0.6);
  --bezel-edge: rgb(255 255 255 / 0.14);
}
.work__head {
  position: relative;
  z-index: 2;
  display: flex;
}
.work__count {
  font-family: var(--body-font);
  font-size: var(--fs-small);
  font-weight: 600;
  letter-spacing: 0.12em;
  vertical-align: top;
  color: var(--text);
}
.work__lede {
  color: var(--text);
}

/* Season switch, in the document's quiet voice: a small-caps label and a
   segmented control, one hairline track holding the four seasons. The
   chosen season is a raised paper chip with a border and a shadow, its text
   bolder and its icon in the accent, so it never relies on colour alone.
   It sits on the lede's row, at the right, and wraps under it when narrow. */
.work__head {
  flex-direction: row;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 16px 32px;
}
.seasons {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 12px;
}
.seasons__label {
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted);
}
.seasons__group {
  display: flex;
  gap: 2px;
  padding: 3px;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: color-mix(in srgb, var(--ink) 4%, transparent);
}
.seasons__btn {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  min-height: 38px;
  padding: 0 14px;
  border: 1px solid transparent;
  border-radius: 9px;
  background: transparent;
  color: var(--text);
  font: inherit;
  font-size: 0.9375rem;
  font-weight: 500;
  cursor: pointer;
  transition: background 0.2s ease, border-color 0.2s ease, color 0.2s ease, box-shadow 0.2s ease;
}
.seasons__btn:hover {
  color: var(--ink);
  background: color-mix(in srgb, var(--ink) 5%, transparent);
}
.seasons__btn:focus-visible {
  outline: 3px solid var(--ink);
  outline-offset: 2px;
}
.seasons__btn[aria-pressed="true"] {
  border-color: var(--line);
  background: var(--paper) 0 0 / 240px auto, var(--bg);
  box-shadow: 0 1px 2px rgb(0 0 0 / 0.08), 0 2px 8px rgb(0 0 0 / 0.06);
  color: var(--ink);
  font-weight: 600;
}
.seasons__btn svg {
  flex: none;
  width: 16px;
  height: 16px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.seasons__btn[aria-pressed="true"] svg {
  stroke: var(--accent-text);
}
.seasons__check {
  display: none;
}
@media (max-width: 520px) {
  .seasons__group {
    width: 100%;
  }
  .seasons__btn {
    flex: 1;
    justify-content: center;
    padding: 0 6px;
  }
}
@media (prefers-reduced-motion: reduce) {
  .seasons__btn {
    transition: none;
  }
}

/* The wall. Column widths and step heights come from the design
   (640 / 400 / 230 px on a 1270 px wall). */
.shelves {
  container-type: inline-size;
  position: relative;
  display: grid;
  max-width: 980px;
  margin: clamp(32px, 4vw, 56px) auto 0;
}
.shelves__list {
  display: grid;
  grid-template-columns: 640fr 400fr minmax(200px, 230fr);
  gap: 3.15cqw;
  align-items: start;
  padding: 0;
  margin: 0;
  list-style: none;
}
/* the long yarn thread over the wall: same cell as the list, on top */
.shelves__list,
.thread {
  grid-area: 1 / 1;
}
.thread {
  z-index: 2;
  width: 100%;
  height: 100%;
  overflow: visible;
  pointer-events: none;
}
.thread path {
  fill: none;
  stroke: #7fa8d8;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.shelf--ku {
  margin-top: 22cqw;
}
.shelf--asians {
  margin-top: 29.3cqw;
}
.shelf__card {
  display: flex;
  flex-direction: column;
  color: var(--ink);
  text-decoration: none;
}
.shelf__card:focus-visible {
  outline: 3px solid var(--ink);
  outline-offset: 8px;
  border-radius: 6px;
}
.shelf__stage {
  display: flex;
  align-items: flex-end;
  gap: 0.4cqw;
  padding-inline: 0.8cqw;
}
.shelf--asians .shelf__stage {
  justify-content: center;
}
.device {
  display: flex;
  flex-direction: column;
  align-items: center;
  transition: translate 0.3s ease;
}
.shelf__card:hover .device,
.shelf__card:focus-visible .device {
  translate: 0 -6px;
}
.device img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.device--monitor {
  width: 84.4%;
}
.device__screen {
  display: block;
  width: 100%;
  padding: 0.95cqw;
  border-radius: 1.1cqw;
  background: #16161a;
  box-shadow: 0 0 0 1px var(--bezel-edge), var(--device-shadow);
}
.device__screen img {
  aspect-ratio: 16 / 9;
  border-radius: 3px;
  background: #000;
}
.device__neck {
  width: 10%;
  height: 3.5cqw;
  background: var(--stand);
}
.device__foot {
  width: 34%;
  height: 0.8cqw;
  border-radius: 6px 6px 0 0;
  background: var(--stand);
}
/* The sleeping cat on top of the Luna monitor. It is the monitor's first
   item, at the right, pulled down so its belly rests on the top of the
   screen and its tail hangs over the edge (drawn above the screen). Sized
   to the monitor. Breathing, floating z's, a tail twitch and an ear flick
   now and then are pure CSS; all of it stops with the pause button and for
   reduced motion. */
.cat {
  position: relative;
  z-index: 1;
  align-self: flex-end;
  width: 22%;
  height: auto;
  margin: 0 3% -6.97% 0; /* its belly line sits on the screen's top edge */
  overflow: visible;
  pointer-events: none;
}
.cat__fur {
  fill: #e6a46a;
}
.cat__inner {
  fill: #f4c9a4;
}
.cat__stripe {
  fill: none;
  stroke: #c47d43;
  stroke-width: 3;
  stroke-linecap: round;
}
.cat__face {
  fill: none;
  stroke: #4a2e1c;
  stroke-width: 1.6;
  stroke-linecap: round;
}
.cat__nose {
  fill: #c9645a;
}
.cat__tail {
  fill: none;
  stroke: #e6a46a;
  stroke-width: 7;
  stroke-linecap: round;
  transform-box: fill-box;
  transform-origin: 0 0;
  animation: cat-tail 9s ease-in-out infinite;
}
.cat__body {
  transform-box: fill-box;
  transform-origin: 50% 100%;
  animation: cat-breathe 3.6s ease-in-out infinite;
}
.cat__head {
  transform-box: fill-box;
  transform-origin: 70% 100%;
  animation: cat-head 3.6s ease-in-out infinite;
}
.cat__ear--right {
  transform-box: fill-box;
  transform-origin: 20% 100%;
  animation: cat-ear 11s ease-in-out infinite;
}
.cat__z {
  font-family: var(--body-font);
  font-size: 16px;
  font-weight: 600;
  fill: var(--text);
  opacity: 0;
  animation: cat-z 4.5s ease-out infinite;
}
.cat__z:nth-child(2) {
  animation-delay: 1.5s;
}
.cat__z:nth-child(3) {
  animation-delay: 3s;
}
@keyframes cat-breathe {
  50% {
    transform: scale(1.015, 1.06);
  }
}
@keyframes cat-head {
  50% {
    transform: translateY(-0.8px);
  }
}
/* the tail rests, then flicks twice near the end of each cycle */
@keyframes cat-tail {
  0%, 80%, 100% {
    transform: rotate(0deg);
  }
  85% {
    transform: rotate(9deg);
  }
  90% {
    transform: rotate(-4deg);
  }
  94% {
    transform: rotate(6deg);
  }
}
@keyframes cat-ear {
  0%, 90%, 100% {
    transform: rotate(0deg);
  }
  93% {
    transform: rotate(-14deg);
  }
  96% {
    transform: rotate(4deg);
  }
}
/* each z drifts up and to the right, growing and fading */
@keyframes cat-z {
  0% {
    opacity: 0;
    transform: translate(0, 0) scale(0.6);
  }
  20% {
    opacity: 0.9;
  }
  100% {
    opacity: 0;
    transform: translate(26px, -30px) scale(1.25);
  }
}
.cat__z {
  transform-box: fill-box;
  transform-origin: 50% 50%;
}
.cat__thread {
  fill: none;
  stroke: #7fa8d8;
  stroke-width: 2;
  stroke-linecap: round;
}
:root.motion-paused .cat :is(.cat__tail, .cat__body, .cat__head, .cat__ear--right, .cat__z) {
  animation-play-state: paused;
}
@media (prefers-reduced-motion: reduce) {
  .cat :is(.cat__tail, .cat__body, .cat__head, .cat__ear--right) {
    animation: none;
  }
  /* one still z, so it still reads as asleep */
  .cat__z {
    animation: none;
  }
  .cat__z:first-child {
    opacity: 0.8;
    transform: translate(12px, -12px);
  }
}

.device--laptop {
  width: 82.5%;
}
.device__lid {
  display: block;
  width: 91%;
  padding: 0.8cqw 0.8cqw 0.95cqw;
  border: 1px solid var(--laptop-edge);
  border-bottom: 0;
  border-radius: 0.8cqw 0.8cqw 0 0;
  background: var(--laptop);
}
.device__lid img {
  aspect-ratio: 16 / 10;
  border-radius: 3px;
}
.device__dots {
  display: flex;
  gap: 0.4cqw;
  padding: 0 0.3cqw 0.6cqw;
}
.device__dots i {
  width: 0.63cqw;
  height: 0.63cqw;
  border-radius: 50%;
  background: var(--laptop-dot);
}
.device__base {
  width: 100%;
  height: 1.1cqw;
  border-radius: 0 0 0.8cqw 0.8cqw;
  background: var(--laptop-base);
}
.device--phone {
  width: 60%;
  padding: 0.63cqw;
  border-radius: 1.75cqw;
  background: #16161a;
  box-shadow: 0 0 0 1px var(--bezel-edge), var(--device-shadow);
}
.device--phone img {
  aspect-ratio: 9 / 19;
  border-radius: 1.25cqw;
}
.shelf__board {
  display: block;
  height: max(14px, 1.75cqw);
  border-radius: 3px;
  background: linear-gradient(var(--wood-top) 0 5px, var(--wood) 5px);
  border-bottom: 2px solid var(--wood-edge);
  box-shadow: var(--board-shadow);
}
.shelf__info {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 20px 0.8cqw 0;
}
/* the name with an arrow that slides on hover, then two tags */
.shelf__name {
  display: inline-flex;
  align-items: center;
  gap: 0.3em;
}
.shelf__name svg {
  width: 0.62em;
  height: 0.62em;
  color: var(--accent-text);
  transition: translate 0.2s ease;
}
.shelf__card:hover .shelf__name svg,
.shelf__card:focus-visible .shelf__name svg {
  translate: 4px 0;
}
.shelf__tags {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}
/* topic tags in neutral grey, so the orange stays for things you act on */
.shelf__tags i {
  padding: 3px 10px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: color-mix(in srgb, var(--ink) 5%, transparent);
  font-size: 0.8125rem;
  font-style: normal;
  font-weight: 500;
  color: var(--text);
  transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease;
}
/* pointing at a project lights its tags up in the accent */
.shelf__card:is(:hover, :focus-visible) .shelf__tags i {
  border-color: color-mix(in srgb, var(--accent) 40%, transparent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  color: var(--accent-text);
}
.shelf__name {
  font-family: var(--heading-font);
  font-size: clamp(1.6rem, 1.1rem + 1.4vw, 2.5rem);
  font-weight: 600;
  line-height: 1.05;
}
.shelf--ku .shelf__name {
  font-size: clamp(1.5rem, 1.1rem + 1vw, 2.1rem);
}
.shelf--asians .shelf__name {
  font-size: clamp(1.4rem, 1.1rem + 0.8vw, 1.9rem);
}

/* Plants. They move a little: the snake plant's leaves sway from their
   base, the succulent breathes, the hanging pothos swings on its cord. */
.plant {
  flex: none;
  overflow: visible;
}
.plant--snake {
  width: 14%;
  margin-left: auto;
}
.plant--succulent {
  width: 13%;
  margin-left: auto;
  transform-origin: 50% 100%;
  animation: plant-breathe 3.6s ease-in-out infinite alternate;
}
.plant--pothos {
  position: absolute;
  top: -2cqw;
  right: 0;
  width: 14cqw;
  transform-origin: 50% 0;
  animation: plant-swing 5s ease-in-out infinite alternate;
}
.plant__cord {
  fill: none;
  stroke: var(--cord);
  stroke-width: 1.5;
}
.plant__vine {
  fill: none;
  stroke: #3f7d4e;
  stroke-width: 2.5;
}
.plant__sway {
  stroke: #b9cf72;
  stroke-width: 1.5;
  transform-box: fill-box;
  transform-origin: 50% 100%;
  animation: plant-sway 4s ease-in-out infinite alternate;
}
.plant__sway--2 {
  animation-duration: 4.8s;
  animation-delay: -1s;
}
.plant__sway--3 {
  animation-duration: 5.4s;
  animation-delay: -2s;
}
.plant__sway--4 {
  animation-duration: 4.4s;
  animation-delay: -0.5s;
}
@keyframes plant-sway {
  from { transform: rotate(-2.5deg); }
  to { transform: rotate(2.5deg); }
}
@keyframes plant-breathe {
  from { transform: scale(1); }
  to { transform: scale(1.06, 1.04); }
}
@keyframes plant-swing {
  from { transform: rotate(-2deg); }
  to { transform: rotate(2deg); }
}
/* season touches: flowers in spring, snow on the pots and shelves in winter */
.plant__spring,
.plant__winter {
  opacity: 0;
  transition: opacity 0.4s ease;
}
.plant__winter {
  fill: #fff;
  stroke: #d7e1ec;
}
.work[data-season="spring"] .plant__spring,
.work[data-season="winter"] .plant__winter {
  opacity: 1;
}
.work[data-season="winter"] .shelf__board {
  background: linear-gradient(#fff 0 5px, var(--wood-top) 5px 8px, var(--wood) 8px);
}

/* What falls. home.js fills this layer and animates each piece with the
   Web Animations API; the side-to-side flutter is CSS. */
.falling {
  position: absolute;
  inset: 0;
  z-index: 1;
  overflow: hidden;
  pointer-events: none;
}
/* dandelion seeds: greyed out in both themes, soft and faint */
.work {
  --seed: rgb(120 118 112 / 0.7);
  --seed-core: rgb(110 104 94 / 0.8);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .work {
    --seed: rgb(190 188 182 / 0.6);
    --seed-core: rgb(170 165 155 / 0.7);
  }
}
:root[data-theme="dark"] .work {
  --seed: rgb(190 188 182 / 0.6);
  --seed-core: rgb(170 165 155 / 0.7);
}
.falling__piece {
  position: absolute;
  top: 0;
  left: 0;
  will-change: transform, opacity;
}
.falling__piece > span {
  display: block;
}
.falling__piece svg {
  display: block;
  width: 100%;
  height: 100%;
  overflow: visible;
}
.flutter {
  animation: flutter 1.6s ease-in-out infinite alternate;
}
.flutter--r {
  animation-name: flutter-r;
}
.drift {
  animation: drift 2.4s ease-in-out infinite alternate;
}
.hover {
  animation: hover 3s ease-in-out infinite alternate;
}
@keyframes flutter {
  from { transform: translateX(-26px) rotate(-35deg); }
  to { transform: translateX(26px) rotate(40deg); }
}
@keyframes flutter-r {
  from { transform: translateX(24px) rotate(30deg); }
  to { transform: translateX(-24px) rotate(-45deg); }
}
@keyframes drift {
  from { transform: translateX(-10px); }
  to { transform: translateX(10px); }
}
@keyframes hover {
  from { transform: translate(-18px, 6px); }
  to { transform: translate(18px, -6px); }
}

/* Phones and narrow windows: the shelves stack, each one centered */
@media (max-width: 860px) {
  .shelves__list {
    grid-template-columns: minmax(0, 1fr);
    gap: 56px;
  }
  .shelf--ku,
  .shelf--asians {
    margin-top: 0;
  }
  .shelf__stage {
    padding-inline: 12px;
  }
  .device--monitor {
    width: 84%;
  }
  .device__screen {
    padding: 8px;
    border-radius: 12px;
  }
  .device__neck {
    height: 28px;
  }
  .device__foot {
    height: 7px;
  }
  .shelf--ku .shelf__stage {
    max-width: 480px;
  }
  .device__lid {
    padding: 8px 8px 10px;
    border-radius: 10px 10px 0 0;
  }
  .device__dots {
    gap: 5px;
    padding: 0 4px 7px;
  }
  .device__dots i {
    width: 7px;
    height: 7px;
  }
  .device__base {
    height: 12px;
    border-radius: 0 0 10px 10px;
  }
  .shelf--asians .shelf__stage {
    max-width: 260px;
    justify-content: center;
  }
  /* each shelf (device, board and label) sits in the middle of the column */
  .shelf__card > * {
    width: 100%;
    margin-inline: auto;
  }
  .shelf--ku .shelf__info {
    max-width: 480px;
  }
  .shelf--asians .shelf__info {
    max-width: 260px;
  }
  .device--phone {
    width: 120px;
    padding: 7px;
    border-radius: 24px;
  }
  .device--phone img {
    border-radius: 18px;
  }
  .shelf--ku .shelf__board,
  .shelf--asians .shelf__board {
    max-width: 480px;
  }
  .shelf--asians .shelf__board {
    max-width: 260px;
  }
  .plant--pothos {
    display: none;
  }
}
@media (prefers-reduced-motion: reduce) {
  .plant--succulent,
  .plant--pothos,
  .plant__sway {
    animation: none;
  }
  .falling {
    display: none;
  }
  .device {
    transition: none;
  }
}

/* a block whose body starts at the left edge, with no indent (Artwork) */
.block--wide .block__body {
  --indent: 0px;
}

/* 5. Artwork and lightbox ---------------------------------------------------- */
.artwork {
  padding-bottom: 36px;
}
.artwork .block {
  padding-top: 0;
}
.artwork__lede {
  margin-top: calc(clamp(24px, 4vh, 40px) * -1 + 4px);
  color: var(--text);
}
.artwork__body {
  margin-top: 0;
  padding-top: 24px;
  border-image: none;
}
/* Dark gallery wall with a spotlight. The art sits in a staggered grid on a
   dark wall; a dark layer (.spot__dark) shares the wall's grid cell on top of
   it, with a soft transparent circle at the cursor (--mx, --my, set by
   home.js), so the art shows only where the light falls. Keyboard focus moves
   the light onto the focused piece. Where there is no hovering pointer (touch
   screens), or for reduced motion, the lights are simply on. Grid only,
   nothing positioned. */
.spot {
  --mx: 50%;
  --my: 40%;
  --r: clamp(140px, 16vw, 230px);
  display: grid;
  margin-top: 24px;
  border-radius: var(--r-xl);
  background: #0b0b0d;
  overflow: hidden;
}
/* the art hangs in columns, each piece at its own shape (masonry, pure CSS
   columns), so nothing is cropped; the columns narrow to two on phones */
.spot__wall {
  columns: 4 200px;
  column-gap: clamp(12px, 1.4vw, 20px);
  padding: clamp(16px, 2.4vw, 40px);
}
.spot__wall li {
  break-inside: avoid;
  margin-bottom: clamp(12px, 1.4vw, 20px);
}
.spot__wall .artwork__item {
  display: block;
  width: 100%;
  padding: 0;
  border: 0;
  border-radius: 10px;
  overflow: hidden;
  background: #16161a;
  cursor: zoom-in;
}
.spot__wall .artwork__item img {
  display: block;
  width: 100%;
  height: auto;
  transition: scale 0.5s ease;
}
.spot__wall .artwork__item:hover img {
  scale: 1.05;
}
.spot__wall .artwork__item:focus-visible {
  outline: 3px solid var(--ink);
  outline-offset: 3px;
}
.spot__dark {
  z-index: 1;
  background: radial-gradient(
    circle var(--r) at var(--mx) var(--my),
    transparent 0,
    rgb(11 11 13 / 0.35) 55%,
    rgb(11 11 13 / 0.94) 100%
  );
  pointer-events: none;
  transition: opacity 0.4s ease;
}
/* before the cursor arrives, a dim glow in the middle invites you in */
.spot:not(.is-lit) .spot__dark {
  --r: clamp(180px, 22vw, 320px);
}
/* The hint sits in the middle of the dark wall (same grid cell, on top) as a
   glowing pill with a light-bulb icon, gently pulsing, until the cursor
   arrives; then it fades away. Only where a hovering pointer exists. */
.spot__wall,
.spot__dark,
.spot__hint {
  grid-area: 1 / 1;
}
.spot__hint {
  z-index: 2;
  place-self: center;
  display: none;
  align-items: center;
  gap: 10px;
  max-width: calc(100% - 32px);
  margin: 0;
  padding: 12px 20px;
  border: 1px solid rgb(255 255 255 / 0.25);
  border-radius: 24px;
  background: rgb(20 20 24 / 0.75);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  box-shadow: 0 0 40px 6px color-mix(in srgb, var(--accent) 35%, transparent);
  color: #f4f4f0;
  font-size: var(--fs-body);
  font-weight: 600;
  line-height: 1.35;
  text-wrap: balance;
  pointer-events: none;
  animation: hint-glow 2.4s ease-in-out infinite;
  transition: opacity 0.4s ease, scale 0.4s ease;
}
.spot__hint svg {
  flex: none;
  width: 22px;
  height: 22px;
  color: var(--accent);
}
.spot.is-lit .spot__hint {
  opacity: 0;
  scale: 0.9;
}
@keyframes hint-glow {
  50% {
    box-shadow: 0 0 60px 14px color-mix(in srgb, var(--accent) 50%, transparent);
  }
}
@media (hover: hover) and (prefers-reduced-motion: no-preference) {
  .spot__hint {
    display: inline-flex;
  }
}
@media (hover: none), (prefers-reduced-motion: reduce) {
  .spot__dark {
    display: none;
  }
}
@media (max-width: 640px) {
  .spot__wall {
    columns: 2;
  }
}

/* Touch screens: no cursor, so scrolling is the flashlight. As the wall
   passes through the screen the light sweeps down it in a slow zigzag,
   staying near the middle of the screen, so each piece lights up as you
   reach it (pure CSS, the wall's own view timeline). The hint says "Scroll"
   and fades once you are on your way. Tapping a piece still opens it. Without
   scroll-driven animations, or with reduced motion, the lights stay on. */
@property --mx {
  syntax: "<length-percentage>";
  inherits: true;
  initial-value: 50%;
}
@property --my {
  syntax: "<length-percentage>";
  inherits: true;
  initial-value: 40%;
}
.spot__say-touch {
  display: none;
}
@media (hover: none) and (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {
    /* the wall clips its corners (overflow: hidden), which makes it a
       scroll box of its own, so the light inside follows the wall's
       timeline in the page instead of its own */
    .spot {
      view-timeline: --wall block;
    }
    .spot__dark {
      display: block;
      --r: clamp(150px, 44vw, 260px);
      animation: spot-sweep linear both;
      animation-timeline: --wall;
    }
    .spot__hint {
      display: inline-flex;
      place-self: start center;
      margin-top: clamp(40px, 12vh, 120px);
      animation: spot-hint-out linear both;
      animation-timeline: --wall;
      animation-range: entry 60% cover 40%;
    }
    .spot__say-mouse {
      display: none;
    }
    .spot__say-touch {
      display: inline;
    }
  }
}
/* the middle of the screen runs from above the wall to below it over the
   wall's cover range; the light follows it, swinging side to side */
@keyframes spot-sweep {
  0% {
    --mx: 50%;
    --my: -15%;
  }
  25% {
    --mx: 25%;
    --my: 22%;
  }
  50% {
    --mx: 72%;
    --my: 50%;
  }
  75% {
    --mx: 28%;
    --my: 78%;
  }
  100% {
    --mx: 50%;
    --my: 115%;
  }
}
@keyframes spot-hint-out {
  to {
    opacity: 0;
    scale: 0.9;
  }
}

.lightbox {
  position: fixed;
  width: 100vw;
  max-width: none;
  height: 100vh;
  max-height: none;
  margin: 0;
  padding: 24px;
  border: 0;
  background: rgb(8 8 10 / 0.98);
  color: #f2f2ee;
}
.lightbox[open] {
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto;
  gap: 16px;
  place-items: center;
}
.lightbox::backdrop {
  background: rgb(8 8 10 / 0.6);
}
.lightbox img {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  border-radius: var(--r-sm);
}
.lightbox__bar {
  display: flex;
  align-items: center;
  gap: 12px;
}
.lightbox .icon-button {
  border-color: rgb(255 255 255 / 0.2);
  background: transparent;
  color: inherit;
}
.lightbox__count {
  min-width: 4em;
  text-align: center;
}

/* Scroll-driven motion ------------------------------------------------------
   In browsers that can tie an animation to the scroll position, the hero swap
   runs in pure CSS on the compositor, exactly in step with the scroll instead
   of a frame behind it. home.js still sets --hero, which the rules above use
   as the fallback elsewhere; animations take precedence, so nothing fights.
   Skipped for reduced motion. */
@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: scroll()) {
    /* Hero: as on Loire, both lines move up one word between 5vh and 20vh
       of scrolling, together, on an ease-out curve. The bottom bar fades out
       over the first few vh. */
    .hero__reel {
      transition: none;
      animation: hero-reel 1ms cubic-bezier(0.33, 1, 0.68, 1) both;
      animation-timeline: scroll(root);
      animation-range: 5vh 20vh;
    }
    .hero__bar {
      opacity: 1;
      animation: hero-bar-out 1ms linear both;
      animation-timeline: scroll(root);
      animation-range: 2vh 7vh;
    }
  }
}
@keyframes hero-reel {
  from {
    translate: 0 0;
  }
  to {
    translate: 0 calc(-1 * var(--word));
  }
}
@keyframes hero-bar-out {
  to {
    opacity: 0;
  }
}

/* Paper: every section's own background carries the grain (the --paper
   token in site.css), so each one reads as paper, in both themes. */
.hello,
.sections > section {
  background: var(--paper) 0 0 / 240px auto, var(--bg);
}
