/* ---- fonts -------------------------------------------------------------
   Inter, self-hosted. Fetching it from Google held the first paint back by
   over a second on a phone; from here it's one preloaded file. Variable, so
   400 and 500 come out of the same one. ---------------------------------- */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 400 500;
  font-display: swap;
  src: url(fonts/inter-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 400 500;
  font-display: swap;
  src: url(fonts/inter-latin-ext.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* ---- tokens ------------------------------------------------------------ */
:root,
html[data-theme="light"] {
  --page:        #ffffff;
  --text-strong: #0e0e0e;
  --text:        #545454;
  --text-muted:  #767676;  /* 4.54:1 on white — AA at 14px */
  --hover:       #f6f6f6;
  --rule:        rgba(0, 0, 0, 0.07);

  /* type scale */
  --font:    13.5px;
  --lh:      21px;
  --font-sm: 12px;

  /* Held at the old scale: cards and the nav links stay put */
  --font-fixed:    12.5px;
  --font-fixed-sm: 11px;
}
html[data-theme="dark"] {
  --page:        #0e0e0e;
  --text-strong: #ffffff;
  --text:        #b0b0b0;
  --text-muted:  #7c7c7c;  /* 4.62:1 on #0e0e0e — AA at 14px */
  --hover:       #171717;
  --rule:        rgba(255, 255, 255, 0.07);
}

html[data-theme="dark"]  { color-scheme: dark; }
html[data-theme="light"] { color-scheme: light; }

/* ---- base -------------------------------------------------------------- */
/* The reset sits in a layer so it loses to any library that ships its CSS
   layered (Yöte does). Unlayered, it beat their padding and flattened the
   fields; everything else on the site is unlayered and wins as before. */
@layer reset, yote;
@layer reset {
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
}

html { -webkit-text-size-adjust: 100%; scrollbar-width: none; }
/* No native scrollbar anywhere, the page included */
html::-webkit-scrollbar { display: none; }

body {
  min-height: 100dvh;
  background: var(--page);
  color: var(--text);
  font-family: Inter, -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", Arial, sans-serif;
  font-size: var(--font);
  line-height: var(--lh);
  font-weight: 400;
  letter-spacing: 0.01em;
  -webkit-font-smoothing: antialiased;
  /* Theme swaps shouldn't strobe */
  transition: background-color 0.3s ease, color 0.3s ease;
}

::selection { background: var(--text-strong); color: var(--page); }

a { color: inherit; }

/* Images carry their real size so the page doesn't jump as they load; the
   attribute sets the ratio, CSS still decides the size */
img[height] { height: auto; }

/* ---- layout ------------------------------------------------------------ */
.content {
  width: 540px;
  max-width: calc(100% - 48px);
  margin: 0 auto;
  padding: 88px 0 72px;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 36px;
}

.avatar {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  object-fit: cover;
  display: block;
}

/* ---- intro ------------------------------------------------------------- */
.intro {
  display: flex;
  flex-direction: column;
  gap: 18px;
  width: 100%;
}
.greeting { color: var(--text-muted); }
.intro em {
  font-style: normal;
  color: var(--text-strong);
}
.intro a,
.connect a,
.study-content a,
.cv-contact a {
  color: var(--text-strong);
  /* A real underline, faint until you reach it: links in running text
     can't rely on colour alone */
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
  text-decoration-color: color-mix(in srgb, currentColor 30%, transparent);
  transition: text-decoration-color 0.18s ease;
}
.intro a:hover,
.connect a:hover,
.study-content a:hover,
.cv-contact a:hover,
.intro a:focus-visible,
.connect a:focus-visible,
.study-content a:focus-visible,
.cv-contact a:focus-visible {
  text-decoration-color: currentColor;
}

/* ---- link lists -------------------------------------------------------- */
.link-section {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 2px;
  width: 100%;
}
.link-section h2 {
  color: var(--text-muted);
  font-size: var(--font);
  line-height: var(--lh);
  font-weight: 400;
  margin-bottom: 8px;
}

.link-row {
  display: flex;
  flex-direction: column;
  /* no wash: rows sit flush with the copy above them */
  /* Negative margin keeps the wash off the text column's alignment */
  padding: 7px 10px;
  margin: 0 -10px;
  border-radius: 6px;
  text-decoration: none;
  background: transparent;
  transition: background-color 0.2s ease;
}
.link-row:hover,
.link-row:focus-visible { outline: none; }
.link-row:hover .row-name,
.link-row:focus-visible .row-name { color: var(--text-strong); }
.link-row:hover .row-meta,
.link-row:focus-visible .row-meta { color: var(--text); }
.row-head {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: baseline;
  gap: 2px 16px;
  width: 100%;
}
.row-name {
  color: var(--text-strong);
  min-width: 0;
}
.row-meta {
  color: var(--text-muted);
  white-space: nowrap;
  transition: color 0.2s ease;
}
.link-row:hover .row-meta { color: var(--text); }

/* ---- connect + footer -------------------------------------------------- */
.connect { gap: 0; }
.connect p { max-width: 100%; }

footer {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 16px;
  width: 100%;
  padding-top: 8px;
  color: var(--text-muted);
}
.clock { white-space: nowrap; }
.clock .tz { margin-right: 6px; }
#clock { font-variant-numeric: tabular-nums; }

/* ---- theme toggle ------------------------------------------------------ */
.theme-toggle {
  position: fixed;
  top: max(20px, env(safe-area-inset-top, 0px));
  right: max(20px, env(safe-area-inset-right, 0px));
  z-index: 10;
  width: 28px;
  height: 28px;
  display: grid;
  place-items: center;
  padding: 0;
  border: none;
  background: none;
  color: var(--text-muted);
  opacity: 0.55;
  cursor: pointer;
  transition: opacity 0.2s ease, color 0.2s ease, transform 0.14s cubic-bezier(0.22, 1, 0.36, 1);
}
.theme-toggle:hover,
.theme-toggle:focus-visible {
  opacity: 1;
  color: var(--text-strong);
  outline: none;
}
.theme-toggle:active { transform: scale(0.92); }
.theme-toggle svg {
  width: 16px;
  height: 16px;
  display: block;
  transition: transform 0.4s cubic-bezier(0.22, 1, 0.36, 1);
}
html[data-theme="dark"] .theme-toggle svg { transform: rotate(180deg); }

/* ---- entrance ---------------------------------------------------------- */
.appear {
  opacity: 0;
  animation: appear 0.8s cubic-bezier(0.22, 1, 0.36, 1) forwards;
  animation-delay: calc(var(--i, 0) * 70ms);
}
@keyframes appear {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: none; }
}
/* What a visitor reads first is painted on the first frame and only rises
   into place: holding it at opacity 0 made the page count as blank for the
   length of the stagger. Everything after it still fades in. */
.intro.appear:first-of-type,
.study-title.appear,
.study-head.appear,
.cv-head.appear,
.cv-contact.appear,
.cv-section.appear {
  opacity: 1;
  animation-name: rise;
}
@keyframes rise {
  from { transform: translateY(8px); }
  to   { transform: none; }
}

/* ---- narrow ------------------------------------------------------------ */
@media (max-width: 600px) {
  .content { padding: 64px 0 56px; gap: 32px; }
  footer { flex-direction: column; align-items: flex-start; gap: 6px; }
}

@media (prefers-reduced-motion: reduce) {
  .appear { opacity: 1; animation: none; }
  *, *::before, *::after { transition-duration: 0.01ms !important; }
}

/* ======================================================================
   Nav — avatar, signature, links
   ====================================================================== */
.nav {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
}
.brand { display: flex; align-items: center; gap: 4px; flex: 1; min-width: 0; }

/* Ring, carried over from v4 */
.avatar-wrap {
  position: relative;
  flex-shrink: 0;
  width: 46px;
  height: 46px;
  padding: 3px;
  border: none;
  border-radius: 50%;
  background: transparent;
  cursor: pointer;
  overflow: hidden;
  transition: background 0.2s ease;
}
.avatar-wrap:hover,
.avatar-wrap:focus-visible {
  background: conic-gradient(from 135deg, #0F6DFF, #00C6FF, #0040FF, #006FFF, #0F6DFF);
  outline: none;
}
/* Gap between ring and photo — picks up the page colour in either theme */
.avatar-wrap::after {
  content: '';
  position: absolute;
  inset: 2px;
  border-radius: 50%;
  background: var(--page);
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.2s ease;
}
.avatar-wrap:hover::after,
.avatar-wrap:focus-visible::after { opacity: 1; }

.avatar-wrap:hover::before {
  animation: none;
  background-position: -200% 0;
  transition: background-position 0.5s ease;
}
.avatar {
  position: relative;
  z-index: 1;
  display: block;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  object-fit: cover;
}

.sig {
  width: 54px;
  height: 54px;
  display: block;
  flex-shrink: 0;
  color: var(--text-strong);
  transition: color 0.3s ease;
}
.sig-path { stroke-dasharray: 1200; stroke-dashoffset: 1200; }

.nav-links { display: flex; align-items: center; gap: 4px; flex-wrap: wrap; }
.nav-links a {
  color: var(--text-muted);
  font-size: var(--font-fixed);
  text-decoration: none;
  white-space: nowrap;
  padding: 6px 8px;
  border-radius: 6px;
  transition: color 0.15s ease, background-color 0.15s ease;
}
.nav-links a:hover,
.nav-links a:focus-visible {
  color: var(--text-strong);
  outline: none;
}

/* ======================================================================
   Now playing
   ====================================================================== */
.player { width: 100%; }
.player-tag {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  width: fit-content;
  max-width: 100%;
  padding: 8px 12px 8px 8px;
  margin-left: -8px;
  border: none;
  border-radius: 999px;
  background: transparent;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: background-color 0.2s ease;
}
.player-tag:hover,
.player-tag:focus-visible { background: var(--hover); outline: none; }

.vinyl {
  position: relative;
  width: 38px;
  height: 38px;
  flex-shrink: 0;
  border-radius: 50%;
  background: repeating-radial-gradient(circle at center,
    #1f1f1f 0px, #1f1f1f 1.5px, #0a0a0a 1.5px, #0a0a0a 3px);
  box-shadow: 0 1px 4px rgba(0,0,0,0.3);
  animation: spin 4s linear infinite;
  animation-play-state: paused;
}
.vinyl.spinning { animation-play-state: running; }
@keyframes spin { to { transform: rotate(360deg); } }

.vinyl-art {
  position: absolute;
  left: 50%; top: 50%;
  width: 46%; height: 46%;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  object-fit: cover;
  background: #333;
  border: 0.5px solid rgba(255,255,255,0.2);
  box-shadow: 0 0 0 1px rgba(0,0,0,0.35);
  z-index: 1;
}
.vinyl-hole {
  position: absolute;
  left: 50%; top: 50%;
  width: 3px; height: 3px;
  border-radius: 50%;
  background: #fff;
  transform: translate(-50%, -50%);
  z-index: 2;
}
.player-lines { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.player-state { color: var(--text-muted); font-size: var(--font-sm); line-height: 1.3; }
.player-track {
  color: var(--text-muted);
  font-size: var(--font-sm);
  line-height: 1.3;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.player-track strong { font-weight: 500; color: var(--text-strong); }

/* ======================================================================
   Lightbox — constrained, not full-bleed
   ====================================================================== */
.lightbox {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  background: color-mix(in srgb, var(--page) 78%, transparent);
  backdrop-filter: blur(20px) saturate(1.1);
  -webkit-backdrop-filter: blur(20px) saturate(1.1);
  opacity: 0;
  transition: opacity 0.2s ease;
}
.lightbox.is-open { opacity: 1; }

.lb-inner {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
  width: 100%;
  max-width: min(520px, 85vw);
}
.lb-img {
  width: 100%;
  max-height: 60vh;
  object-fit: contain;
  box-shadow: 0 12px 48px rgba(0, 0, 0, 0.12);
}
.lb-nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 32px;
  height: 32px;
  border: 1px solid var(--rule);
  border-radius: 50%;
  background: var(--page);
  color: var(--text-strong);
  font-size: 16px;
  line-height: 1;
  cursor: pointer;
  display: grid;
  place-items: center;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
  transition: transform 0.15s ease;
}
.lb-nav:hover { transform: translateY(-50%) scale(1.08); }
.lb-prev { left: -44px; }
.lb-next { right: -44px; }

.lb-close {
  position: absolute;
  top: -40px;
  right: 0;
  width: 28px;
  height: 28px;
  border: none;
  background: none;
  color: var(--text-muted);
  cursor: pointer;
  display: grid;
  place-items: center;
  transition: color 0.15s ease;
}
.lb-close:hover { color: var(--text-strong); }

.lb-dots { display: flex; gap: 5px; align-items: center; }
.lb-dot {
  width: 5px;
  height: 5px;
  padding: 0;
  border: none;
  border-radius: 50%;
  background: var(--text-muted);
  opacity: 0.4;
  cursor: pointer;
  transition: opacity 0.15s ease, transform 0.15s ease;
}
.lb-dot:hover { opacity: 0.7; }
.lb-dot.is-on { opacity: 1; background: var(--text-strong); transform: scale(1.3); }

@media (max-width: 680px) {
  .lb-prev { left: 8px; }
  .lb-next { right: 8px; }
  .lb-img  { max-height: 65vh; }
  .lb-inner { max-width: 92vw; }
}

/* ======================================================================
   About — breadcrumb, collage, strips
   ====================================================================== */
.crumb { display: flex; align-items: center; gap: 8px; color: var(--text-muted); }
.crumb a { color: var(--text-muted); text-decoration: none; transition: color 0.15s ease; }
.crumb a:hover { color: var(--text-strong); }
.crumb [aria-current] { color: var(--text-strong); }

.section-label {
  color: var(--text-muted);
  font-size: var(--font);
  line-height: var(--lh);
  font-weight: 400;
  margin-bottom: -6px;
}

.collage-section, .strip-section { width: 100%; }

/* Full-bleed out of the 540px column so the canvas has room */
.collage-outer, .movie-strip, .album-grid {
  width: 100vw;
  max-width: 100vw;
  margin-left: calc(50% - 50vw);
  padding-inline: max(24px, calc(50vw - 270px));
  overflow-x: auto;
  scrollbar-width: none;
}
.collage-outer::-webkit-scrollbar,
.movie-strip::-webkit-scrollbar,
.album-grid::-webkit-scrollbar { display: none; }

.collage-outer { padding-top: 56px; padding-bottom: 16px; }
.collage-canvas {
  position: relative;
  width: 880px;
  height: 420px;
  flex-shrink: 0;
}
.collage-item {
  position: absolute;
  cursor: grab;
  will-change: transform;
  touch-action: none;
}
.collage-item.is-dragging { cursor: grabbing; }
.collage-img {
  display: block;
  max-width: none;
  opacity: 0;
  transform: scale(0.88);
  transition: opacity 0.4s ease, transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
  pointer-events: none;
  user-select: none;
}
.collage-img.is-in { opacity: 1; transform: none; }
.collage-hint { color: var(--text-muted); margin-top: 4px; }

.movie-strip, .album-grid { display: flex; gap: 12px; padding-block: 4px; }
.movie {
  display: flex;
  flex-direction: column;
  gap: 8px;
  width: 136px;
  flex-shrink: 0;
  text-decoration: none;
}
.movie img {
  width: 136px;
  height: 196px;
  object-fit: cover;
  border-radius: 6px;
  display: block;
  transition: transform 0.3s cubic-bezier(0.22, 1, 0.36, 1);
}
.movie:hover img { transform: translateY(-4px); }
.movie-title { color: var(--text-muted); font-size: var(--font-sm); line-height: 1.35; transition: color 0.15s ease; }
.movie:hover .movie-title { color: var(--text-strong); }

.album { width: 104px; height: 104px; flex-shrink: 0; display: block; }
.album img {
  width: 104px;
  height: 104px;
  object-fit: cover;
  border-radius: 4px;
  display: block;
  transition: transform 0.3s cubic-bezier(0.22, 1, 0.36, 1);
}
a.album:hover img { transform: translateY(-4px); }

@media (max-width: 600px) {
  .nav { flex-direction: column; align-items: flex-start; gap: 12px; }
  .nav-links { margin-left: -8px; }
  .collage-canvas { height: 380px; }
}


/* ======================================================================
   Greeting — rotates through languages, click to advance
   ====================================================================== */
.greeting {
  display: inline-block;
  padding: 0;
  border: none;
  background: none;
  color: var(--text-muted);
  font: inherit;
  letter-spacing: inherit;
  text-align: left;
  cursor: pointer;
  transition: color 0.15s ease;
}
.greeting:hover, .greeting:focus-visible { color: var(--text-strong); outline: none; }
.greeting-text {
  display: inline-block;
  transition: opacity 0.18s ease, transform 0.18s ease;
}
.greeting-text.is-out { opacity: 0; transform: translateY(-3px); }

/* ======================================================================
   Chrome cluster — audio + theme
   ====================================================================== */
.chrome {
  position: fixed;
  top: max(20px, env(safe-area-inset-top, 0px));
  right: max(20px, env(safe-area-inset-right, 0px));
  z-index: 10;
  display: flex;
  align-items: center;
  gap: 4px;
}
.chrome .theme-toggle { position: static; top: auto; right: auto; }

.audio-toggle,
.sticker-reset {
  width: 28px;
  height: 28px;
  display: grid;
  place-items: center;
  padding: 0;
  border: none;
  background: none;
  color: var(--text-muted);
  opacity: 0.55;
  cursor: pointer;
  transition: opacity 0.2s ease, color 0.2s ease, transform 0.14s cubic-bezier(0.22, 1, 0.36, 1);
}
.audio-toggle:hover, .audio-toggle:focus-visible,
.sticker-reset:not(:disabled):hover, .sticker-reset:focus-visible { opacity: 1; color: var(--text-strong); outline: none; }
.audio-toggle:active, .sticker-reset:not(:disabled):active { transform: scale(0.92); }
.audio-toggle svg, .sticker-reset svg { width: 16px; height: 16px; display: block; }
/* Nothing to put back until something has moved */
.sticker-reset:disabled { opacity: 0.22; cursor: default; }
/* its display:grid would otherwise beat the hidden attribute on a phone */
.sticker-reset[hidden] { display: none; }
.audio-toggle .ico-on  { display: none; }
.audio-toggle .ico-off { display: block; }
.audio-toggle[aria-pressed="true"] .ico-on  { display: block; }
.audio-toggle[aria-pressed="true"] .ico-off { display: none; }

@media (prefers-reduced-motion: reduce) {
}


/* ======================================================================
   Project rows — the icon is the hover indicator
   ====================================================================== */
/* Names start flush with the section labels; the mark hangs outside the
   column in the left margin, so nothing indents and nothing shifts */
.row-head { position: relative; }

.row-mark {
  position: absolute;
  left: -26px;
  top: 0;
  width: 16px;
  height: var(--lh);
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  transform: scale(0.7);
  transition: opacity 0.22s ease, transform 0.28s cubic-bezier(0.34, 1.4, 0.64, 1);
}
.row-mark img {
  width: 16px;
  height: 16px;
  object-fit: contain;
  display: block;
}
.is-project:hover .row-mark,
.is-project:focus-visible .row-mark { opacity: 1; transform: none; }

/* ======================================================================
   Gallery strip — avatar opens it inline, pushing the page down
   ====================================================================== */
.gallery {
  width: 100%;
  overflow: hidden;
  max-height: 0;
  transition: max-height 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}
.gallery.is-open { max-height: 230px; }

.gallery-track {
  display: flex;
  gap: 8px;
  padding: 16px 0;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}
.gallery-track::-webkit-scrollbar { display: none; }

.gallery-card {
  width: 149px;
  height: 190px;
  flex-shrink: 0;
  overflow: hidden;
  opacity: 0;
  transform: translateY(-24px);
  transition: opacity 0.4s ease, transform 0.4s cubic-bezier(0.34, 1.3, 0.64, 1);
  scroll-snap-align: start;
  border: none;
  padding: 0;
  background: none;
  cursor: pointer;
}
.gallery-card.is-in { opacity: 1; transform: none; }
.gallery-card img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform 0.3s ease;
}
.gallery-card:hover img { transform: scale(1.04); }

/* Avatar keeps its ring while the strip is open */
.avatar-wrap.is-active {
  background: conic-gradient(from 135deg, #0F6DFF, #00C6FF, #0040FF, #006FFF, #0F6DFF);
}
.avatar-wrap.is-active::after { opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  .gallery { transition: none; }
  .gallery-card { transition: none; opacity: 1; transform: none; }
  .row-mark { transition: none; }
}

/* The left margin runs out on narrow screens, and there's no hover there
   anyway, so the mark simply doesn't appear */
@media (max-width: 640px) {
  .row-mark { display: none; }
}


/* ======================================================================
   Project cards - layered oklch gradients + grain
   Method from jakub.kr/work/gradients: stack a couple of soft radial
   lights over a linear body and blend them, interpolating `in oklch` so
   the hue travels angularly and picks up tones between the stops.
   Each card's three hues are its brand hue plus two neighbours.
   ====================================================================== */
.cards-section { width: 100%; }

.cards {
  display: flex;
  gap: 14px;
  width: 100vw;
  max-width: 100vw;
  margin-left: calc(50% - 50vw);
  padding-inline: max(24px, calc(50vw - 270px));
  padding-block: 4px;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
}
.cards::-webkit-scrollbar { display: none; }

.card {
  position: relative;
  flex-shrink: 0;
  width: 208px;
  aspect-ratio: 5 / 6;
  border-radius: 10px;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  padding: 16px;
  text-decoration: none;
  isolation: isolate;
  transition: transform 0.35s cubic-bezier(0.22, 1, 0.36, 1);
}
.card:hover { transform: translateY(-4px); }

/* Grain. feTurbulence rendered once as a data URI, tiled and blended so it
   sits in the colour rather than on top of it. */
.card::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  opacity: 0.6;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.7' numOctaves='5' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 160px 160px;
}

/* Everything readable sits above the grain */
.card-mark, .card-body { position: relative; z-index: 2; }

.card-mark { width: 26px; height: 26px; display: block; }
.card-mark img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
  /* Marks go monochrome white so five different brand palettes don't
     fight the five different card gradients */
  filter: brightness(0) invert(1);
  opacity: 0.95;
}

.card-body { display: flex; flex-direction: column; gap: 6px; }
.card-name {
  color: #fff;
  font-size: var(--font-fixed);
  font-weight: 500;
  line-height: 1.25;
}
.card-desc {
  color: rgba(255, 255, 255, 0.95);
  font-size: var(--font-fixed-sm);
  line-height: 1.38;
}

.card-costgraph {
  --vg: 0.16;
  background:
    radial-gradient(ellipse 66% 32% at var(--l3x) var(--l3y), oklch(0.68 0.148 292 / 0.3), transparent 64%),
    radial-gradient(ellipse 96% 52% at var(--l1x) var(--l1y), oklch(0.52 0.276 292 / 0.7), transparent 60%),
    radial-gradient(ellipse 88% 48% at var(--l2x) var(--l2y), oklch(0.72 0.136 230 / 0.66), transparent 58%),
    linear-gradient(in oklch 165deg, oklch(0.7 0.157 248) 0%, oklch(0.57 0.221 260) 50%, oklch(0.32 0.19 276) 100%);
}
.card-luotain {
  --vg: 0.36;
  background:
    radial-gradient(ellipse 66% 32% at var(--l3x) var(--l3y), oklch(0.86 0.09 72 / 0.3), transparent 64%),
    radial-gradient(ellipse 96% 52% at var(--l1x) var(--l1y), oklch(0.79 0.161 72 / 0.7), transparent 60%),
    radial-gradient(ellipse 88% 48% at var(--l2x) var(--l2y), oklch(0.59 0.225 18 / 0.66), transparent 58%),
    linear-gradient(in oklch 165deg, oklch(0.67 0.208 32) 0%, oklch(0.7 0.191 44) 50%, oklch(0.5 0.112 60) 100%);
}
.card-yote {
  --vg: 0.3;
  background:
    radial-gradient(ellipse 66% 32% at var(--l3x) var(--l3y), oklch(0.78 0.022 286 / 0.34), transparent 64%),
    radial-gradient(ellipse 96% 52% at var(--l1x) var(--l1y), oklch(0.56 0.03 286 / 0.7), transparent 60%),
    radial-gradient(ellipse 88% 48% at var(--l2x) var(--l2y), oklch(0.42 0.028 240 / 0.66), transparent 58%),
    linear-gradient(in oklch 165deg, oklch(0.54 0.026 262) 0%, oklch(0.39 0.022 268) 52%, oklch(0.25 0.016 274) 100%);
}
.card-lonar {
  --vg: 0.2;
  background:
    radial-gradient(ellipse 66% 32% at var(--l3x) var(--l3y), oklch(0.75 0.134 358 / 0.3), transparent 64%),
    radial-gradient(ellipse 96% 52% at var(--l1x) var(--l1y), oklch(0.65 0.251 358 / 0.7), transparent 60%),
    radial-gradient(ellipse 88% 48% at var(--l2x) var(--l2y), oklch(0.64 0.191 40 / 0.66), transparent 58%),
    linear-gradient(in oklch 165deg, oklch(0.66 0.225 8) 0%, oklch(0.63 0.24 20) 50%, oklch(0.44 0.143 36) 100%);
}
.card-kernui {
  --vg: 0.16;
  background:
    radial-gradient(ellipse 66% 32% at var(--l3x) var(--l3y), oklch(0.76 0.158 319 / 0.3), transparent 64%),
    radial-gradient(ellipse 96% 52% at var(--l1x) var(--l1y), oklch(0.64 0.293 319 / 0.7), transparent 60%),
    radial-gradient(ellipse 88% 48% at var(--l2x) var(--l2y), oklch(0.52 0.214 261 / 0.66), transparent 58%),
    linear-gradient(in oklch 165deg, oklch(0.52 0.258 277) 0%, oklch(0.51 0.276 289) 50%, oklch(0.38 0.188 305) 100%);
}

@media (prefers-reduced-motion: reduce) {
  .card { transition: none; }
}

.cards-label {
  color: var(--text-muted);
  font-size: var(--font);
  line-height: var(--lh);
  font-weight: 400;
  margin-bottom: 8px;
}

/* ======================================================================
   Drifting lights
   Registered so the browser can interpolate them; without @property these
   resolve as static values and the gradient simply sits still.
   ====================================================================== */
@property --l1x { syntax: '<percentage>'; inherits: true; initial-value: 76%; }
@property --l1y { syntax: '<percentage>'; inherits: true; initial-value: 6%; }
@property --l2x { syntax: '<percentage>'; inherits: true; initial-value: 8%; }
@property --l2y { syntax: '<percentage>'; inherits: true; initial-value: 30%; }
@property --l3x { syntax: '<percentage>'; inherits: true; initial-value: 52%; }
@property --l3y { syntax: '<percentage>'; inherits: true; initial-value: 22%; }

:root {
  --l1x: 76%; --l1y: 6%;
  --l2x: 8%;  --l2y: 30%;
  --l3x: 52%; --l3y: 22%;
}

/* Three different paths per light. Four stops rather than two, so a light
   traces a loop around the card instead of sliding back and forth along
   the same line. */
@keyframes l1-a {
  0%,100% { --l1x: 78%; --l1y: 8%; }
  25%     { --l1x: 30%; --l1y: 26%; }
  50%     { --l1x: 12%; --l1y: 62%; }
  75%     { --l1x: 58%; --l1y: 40%; }
}
@keyframes l1-b {
  0%,100% { --l1x: 14%; --l1y: 18%; }
  33%     { --l1x: 86%; --l1y: 44%; }
  66%     { --l1x: 46%; --l1y: 66%; }
}
@keyframes l1-c {
  0%,100% { --l1x: 50%; --l1y: 64%; }
  30%     { --l1x: 88%; --l1y: 12%; }
  70%     { --l1x: 18%; --l1y: 34%; }
}

@keyframes l2-a {
  0%,100% { --l2x: 10%; --l2y: 34%; }
  30%     { --l2x: 62%; --l2y: 66%; }
  65%     { --l2x: 90%; --l2y: 16%; }
}
@keyframes l2-b {
  0%,100% { --l2x: 88%; --l2y: 58%; }
  25%     { --l2x: 44%; --l2y: 10%; }
  50%     { --l2x: 6%;  --l2y: 46%; }
  75%     { --l2x: 70%; --l2y: 30%; }
}
@keyframes l2-c {
  0%,100% { --l2x: 34%; --l2y: 12%; }
  40%     { --l2x: 8%;  --l2y: 60%; }
  75%     { --l2x: 76%; --l2y: 52%; }
}

@keyframes l3-a {
  0%,100% { --l3x: 54%; --l3y: 20%; }
  35%     { --l3x: 16%; --l3y: 52%; }
  70%     { --l3x: 84%; --l3y: 62%; }
}
@keyframes l3-b {
  0%,100% { --l3x: 20%; --l3y: 62%; }
  25%     { --l3x: 72%; --l3y: 24%; }
  55%     { --l3x: 92%; --l3y: 54%; }
  80%     { --l3x: 40%; --l3y: 6%; }
}
@keyframes l3-c {
  0%,100% { --l3x: 90%; --l3y: 36%; }
  45%     { --l3x: 24%; --l3y: 14%; }
  75%     { --l3x: 56%; --l3y: 68%; }
}

/* Each card gets its own combination of paths, its own three periods, and a
   negative delay so it starts mid-loop. Nothing shares a period with
   anything else, and none of the periods divide evenly into another, so the
   three lights on one card never resynchronise and no two cards match. */
.card {
  animation:
    var(--a1, l1-a) var(--d1, 19s) ease-in-out var(--o1, 0s) infinite,
    var(--a2, l2-a) var(--d2, 23s) ease-in-out var(--o2, 0s) infinite,
    var(--a3, l3-a) var(--d3, 29s) ease-in-out var(--o3, 0s) infinite;
}

.card-costgraph { --a1: l1-a; --a2: l2-b; --a3: l3-c; --d1: 21s; --d2: 34s; --d3: 27s; --o1: -4s;  --o2: -17s; --o3: -9s;  }
.card-luotain   { --a1: l1-c; --a2: l2-a; --a3: l3-b; --d1: 31s; --d2: 19s; --d3: 41s; --o1: -13s; --o2: -6s;  --o3: -22s; }
.card-yote      { --a1: l1-b; --a2: l2-c; --a3: l3-a; --d1: 26s; --d2: 44s; --d3: 33s; --o1: -21s; --o2: -2s;  --o3: -15s; }
.card-lonar     { --a1: l1-a; --a2: l2-c; --a3: l3-b; --d1: 37s; --d2: 23s; --d3: 29s; --o1: -8s;  --o2: -26s; --o3: -3s;  }
.card-kernui    { --a1: l1-c; --a2: l2-b; --a3: l3-a; --d1: 24s; --d2: 39s; --d3: 46s; --o1: -18s; --o2: -11s; --o3: -31s; }

/* With the lights free to wander into the lower half, the text can no
   longer sit on the body's bottom stop alone. This vignette is what it sits
   on instead: it fades in from 58% and reaches 0.72 at the floor, which
   holds white above 4.5:1 even against the brightest a light can push the
   bottom of a card. */
.card::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background: linear-gradient(to top,
    rgba(0, 0, 0, var(--vg, 0.34)) 0%,
    rgba(0, 0, 0, calc(var(--vg, 0.34) * 0.47)) 28%,
    transparent 62%);
}

@media (prefers-reduced-motion: reduce) {
  .card { animation: none; }
}



/* Greeting stays lowercase whatever language it lands on, without
   mangling the source text or its lang attribute */
.greeting-text { text-transform: lowercase; }

/* ======================================================================
   Case study pages
   Same shell as the homepage. The hero sits the real product screenshot
   on that project's own gradient, so the page opens on the work itself.
   ====================================================================== */
.crumb { display: flex; align-items: center; gap: 8px; color: var(--text-muted); }
.crumb a { color: var(--text-muted); text-decoration: none; transition: color 0.15s ease; }
.crumb a:hover { color: var(--text-strong); }
.crumb [aria-current] { color: var(--text-strong); }

.study-head { display: flex; flex-direction: column; gap: 12px; width: 100%; }

.study-hero {
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
  border-radius: 10px;
  overflow: hidden;
  display: grid;
  place-items: center;
  padding: 26px 26px 0;
}
.study-hero img,
.study-hero video {
  width: 100%;
  max-height: 100%;
  object-fit: cover;
  object-position: top center;
  border-radius: 6px 6px 0 0;
  box-shadow: 0 18px 40px -12px rgba(0, 0, 0, 0.45);
  display: block;
}

.study-pills { display: flex; gap: 4px; flex-wrap: wrap; }
.study-pill {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 4px 10px;
  border-radius: 8px;
  background: var(--hover);
  color: var(--text-strong);
  font-size: var(--font-sm);
  line-height: 1.4;
  text-decoration: none;
  white-space: nowrap;
}
a.study-pill { padding-left: 6px; }
a.study-pill:hover { background: color-mix(in srgb, var(--hover) 68%, var(--text-muted)); }
.study-pill-mark { width: 14px; height: 14px; flex-shrink: 0; }
.study-pill-mark img { width: 100%; height: 100%; object-fit: contain; display: block; }

.study-section { display: flex; flex-direction: column; gap: 12px; width: 100%; }
.study-label { color: var(--text-muted); font-size: var(--font); line-height: var(--lh); font-weight: 400; }

/* Read by screen readers, not shown */
.sr-only { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
.study-content {
  display: flex;
  flex-direction: column;
  gap: 10px;
  color: var(--text-strong);
  max-width: 620px;
}
.study-content ul { padding-left: 20px; display: flex; flex-direction: column; gap: 4px; }


@media (max-width: 600px) {
  .study-hero { aspect-ratio: 4 / 3; padding: 16px 16px 0; }
}

/* Sub-headings inside a study, so a long section reads as parts rather
   than one run of prose */
.study-subhead {
  font-size: var(--font);
  line-height: var(--lh);
  font-weight: 500;
  color: var(--text-strong);
  margin-top: 10px;
}
.study-content > .study-subhead:first-child { margin-top: 0; }

/* A staged figure, as the case studies have it: a neutral platform with one
   or two shots sitting on it. Height is fixed and images are contained, so a
   tall mobile screenshot sits inside the stage instead of running to 1600px
   at full column width. */
.study-figure {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 14px;
  width: 100%;
  max-width: none;
  height: 300px;
  margin: 6px 0;
  padding: 18px;
  background: var(--hover);
  border-radius: 8px;
  overflow: hidden;
}
.study-figure img,
.study-figure video {
  height: 100%;
  width: auto;
  max-width: 100%;
  object-fit: contain;
  border-radius: 4px;
  display: block;
}
/* A lone wide shot can use the full stage rather than being boxed in */
.study-figure[data-n="1"] img,
.study-figure[data-n="1"] video { max-width: 100%; }

@media (max-width: 600px) {
  .study-figure { height: 220px; padding: 12px; gap: 10px; }
}

/* SVG feature panels are drawn on white, so the grey screenshot stage shows
   as a halo around them */
.study-figure:has(img[src$=".svg"]) { background: transparent; padding: 0; height: auto; align-items: stretch; }
/* Panels are shown whole at their own aspect: a fixed-height stage cropped
   them mid-word, and side by side each takes an equal share of the row */
.study-figure:has(img[src$=".svg"]) img { border-radius: 8px; flex: 1 1 0; min-width: 0; width: 100%; height: auto; }

/* ======================================================================
   Study header
   A study used to open on a screenshot: no name, no one-liner, no role or
   year above the fold, and the site link arriving small and late. This puts
   all four first, which also means the intro no longer has to explain what
   the thing is before the voice can start.
   ====================================================================== */
.study-title { display: flex; flex-direction: column; gap: 10px; width: 100%; }
.study-title h1 {
  font-size: 26px;
  line-height: 1.2;
  font-weight: 500;
  letter-spacing: -0.01em;
  color: var(--text-strong);
}
.study-lede {
  max-width: 560px;
  color: var(--text);
}
.study-title .study-pills { margin-top: 2px; }

/* A study used to stop dead and drop into the articles block. */
.study-next {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  width: 100%;
  padding: 16px 0;
  border-top: 1px solid var(--rule);
  text-decoration: none;
  color: inherit;
}
.study-next-label { color: var(--text-muted); }
.study-next-name {
  color: var(--text-strong);
  font-weight: 500;
  transition: opacity 0.15s ease;
}
.study-next:hover .study-next-name { opacity: 0.65; }

@media (max-width: 600px) {
  .study-title h1 { font-size: 22px; }
}

/* ======================================================================
   Sound note
   One line on arrival, because a site that makes noise should say so
   before it makes any. It pushes the page and the toggles down rather
   than covering them, so the speaker it points at stays reachable while
   the note is up.
   ====================================================================== */
.sound-note {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 20;
  height: 32px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0 46px;
  /* 6.06:1 against white — AA with room to spare at this size */
  background: #1257e0;
  color: #fff;
  font-size: var(--font-sm);
  line-height: 1;
  transform: translateY(-100%);
  transition: transform 0.5s cubic-bezier(0.22, 1, 0.36, 1);
}
body.has-note .sound-note { transform: none; }

.sound-note button {
  font: inherit;
  color: inherit;
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
}
.sound-note-off {
  text-decoration: underline;
  text-underline-offset: 2px;
  text-decoration-thickness: 1px;
  opacity: 0.88;
  transition: opacity 0.2s ease;
}
.sound-note-off:hover, .sound-note-off:focus-visible { opacity: 1; outline: none; }

.sound-note-x {
  position: absolute;
  right: max(12px, env(safe-area-inset-right, 0px));
  top: 50%;
  translate: 0 -50%;
  width: 24px;
  height: 24px;
  display: grid;
  place-items: center;
  opacity: 0.7;
  transition: opacity 0.2s ease;
}
.sound-note-x:hover, .sound-note-x:focus-visible { opacity: 1; outline: none; }
.sound-note-x svg { width: 10px; height: 10px; display: block; }

/* The page and the fixed toggles come down with it */
body { transition: background-color 0.3s ease, color 0.3s ease,
                   padding-top 0.5s cubic-bezier(0.22, 1, 0.36, 1); }
body.has-note { padding-top: 32px; }
.chrome, .theme-toggle { transition: top 0.5s cubic-bezier(0.22, 1, 0.36, 1); }
body.has-note .chrome,
body.has-note .theme-toggle { top: calc(max(20px, env(safe-area-inset-top, 0px)) + 32px); }

@media (max-width: 560px) {
  /* The speaker is still up there; on a phone the sentence is the part
     that has to survive */
  .sound-note-where { display: none; }
  .sound-note { padding: 0 40px 0 16px; }
}
@media (prefers-reduced-motion: reduce) {
  .sound-note, body, .chrome, .theme-toggle { transition: none; }
}

/* ======================================================================
   Stickers
   Peeking in from the far edges, loose on the page. The die-cut border
   and the rim shade are drawn once per sticker on a canvas (app.js), so
   the white follows each shape exactly at the screen's pixel density.
   ====================================================================== */
body { position: relative; }

.stickers {
  position: absolute;
  inset: 0;
  z-index: 5;                 /* over the page, under the toggles and the note */
  overflow: hidden;           /* hanging off the edge must never widen the page */
  overflow: clip;
  pointer-events: none;
}

.sticker {
  position: absolute;
  top: 0;
  left: 0;
  pointer-events: auto;
  cursor: grab;
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
  will-change: transform;
  /* Two shadows: one tight contact shadow so the white edge holds on a
     white page, one soft one for how far it sits off it */
  filter: drop-shadow(0 1px 1px rgb(0 0 0 / 0.14))
          drop-shadow(0 5px 10px rgb(0 0 0 / 0.09));
  transition: filter 0.22s cubic-bezier(0.22, 1, 0.36, 1);
}
.sticker.is-held {
  cursor: grabbing;
  filter: drop-shadow(0 2px 3px rgb(0 0 0 / 0.12))
          drop-shadow(0 18px 26px rgb(0 0 0 / 0.2));
}
/* The canvas carries the die-cut border past the sticker's own box */
.sticker canvas {
  display: block;
  max-width: none;
  pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
  .sticker { transition: none; }
}

/* ======================================================================
   Yöte study
   The live fields sit on the same grey stage the screenshots use. Yöte's
   own surface and text tokens are pointed at the site's, at the stage, so
   the fields read as part of the page in both themes; the purple accent is
   the product's and stays. The focus rings are composed from bg-default, so
   they are restated here or they'd keep Yöte's baked-in ring.
   ====================================================================== */
.study-content code {
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-size: 0.88em;
  padding: 1px 5px;
  border-radius: 4px;
  background: var(--hover);
  color: var(--text-strong);
  overflow-wrap: anywhere;
}
.study-content em { font-style: italic; }

.study-figure-tall { height: auto; padding: 18px; }
.study-figure-tall img { width: 100%; height: auto; }

.yote-stage {
  display: flex;
  flex-direction: column;
  gap: 32px;
  width: 100%;
  margin: 6px 0;
  padding: 36px 24px;
  border-radius: 8px;
  background: var(--hover);
  /* the widest field is the 380px code row; keep every field on that column */
  align-items: center;
}
.yote-slot { width: 100%; max-width: 380px; }
.yote-stage {
  --yote-bg-default: var(--page);
  --yote-text-strong: var(--text-strong);
  --yote-popover-bg: var(--page);
  --yote-focus-active: 0 0 0 2px var(--yote-bg-default), 0 0 0 4px var(--yote-purple-alpha-24);
  --yote-focus-error: 0 0 0 2px var(--yote-bg-default), 0 0 0 4px var(--yote-error-faint);
  font-size: 14px;
}
html[data-theme="dark"] .yote-stage {
  --yote-bg-surface: #1c1c1c;
  --yote-popover-bg: #1c1c1c;
}
@media (max-width: 600px) {
  .yote-stage { padding: 24px 16px; gap: 26px; }
}

/* ======================================================================
   Playground (explorations.html)
   A dense mosaic, as the old page had it: two columns of 4:3 tiles that
   break out of the 540px column to 980px, one column on a phone. Tiles
   open in the lightbox (explorations.js), which steps through the lot.
   ====================================================================== */
.explore-sites a.study-pill { padding-left: 10px; }
.explore-sites svg { color: var(--text-muted); flex-shrink: 0; }

.explore-section { width: 100%; }
.explore-grid {
  --explore-w: min(980px, calc(100vw - 32px));
  list-style: none;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 6px;
  width: var(--explore-w);
  /* centred on the column, whatever the column's own width */
  margin-left: calc((100% - var(--explore-w)) / 2);
}
.explore-cell {
  position: relative;
  aspect-ratio: 4 / 3;
  border-radius: 6px;
  overflow: hidden;
  background: var(--hover);
  isolation: isolate;
}
.explore-open {
  display: block;
  width: 100%;
  height: 100%;
  padding: 0;
  border: none;
  background: none;
  cursor: zoom-in;
}
.explore-open:focus-visible { outline: 2px solid var(--text-strong); outline-offset: -2px; }
.explore-open img,
.explore-open video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform 0.4s cubic-bezier(0.2, 0.88, 0.24, 1);
}
.explore-open:hover img,
.explore-open:hover video { transform: scale(1.02); }

/* Shimmer until the tile's image (or a video's poster) arrives. Only once
   the script is there to take it away again. */
.explore-grid.is-enhanced .explore-cell::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background: linear-gradient(90deg, transparent 0%,
    color-mix(in srgb, var(--page) 55%, transparent) 50%, transparent 100%);
  background-size: 200% 100%;
  animation: explore-shimmer 1.4s infinite;
  transition: opacity 0.3s ease;
}
.explore-grid.is-enhanced .explore-cell.is-loaded::before { opacity: 0; animation: none; }
@keyframes explore-shimmer {
  0%   { background-position: -200% 0; }
  100% { background-position:  200% 0; }
}

@media (max-width: 680px) {
  .explore-grid { grid-template-columns: minmax(0, 1fr); }
}

/* The shared lightbox, widened for screenshots and recordings */
.lightbox--wide .lb-inner { max-width: min(1100px, 92vw); gap: 12px; }
.lb-stage { display: flex; align-items: center; justify-content: center; width: 100%; }
.lightbox--wide .lb-img,
.lb-video {
  width: auto;
  max-width: 100%;
  max-height: 78vh;
  object-fit: contain;
  border-radius: 6px;
  box-shadow: 0 12px 48px rgba(0, 0, 0, 0.12);
  display: block;
}
.lb-video { background: var(--hover); }
/* Sat over the media rather than hanging off it, so they stay on screen */
.lightbox--wide .lb-prev { left: 8px; }
.lightbox--wide .lb-next { right: 8px; }
.lb-count {
  color: var(--text-muted);
  font-size: var(--font-sm);
  font-variant-numeric: tabular-nums;
}

@media (prefers-reduced-motion: reduce) {
  .explore-grid.is-enhanced .explore-cell::before { animation: none; }
  .explore-open img, .explore-open video { transition: none; }
}

/* ======================================================================
   Writing (articles/*.html)
   The old article hero: a figure on a yellow gradient with four textures
   soft-lit over it. Positions are the original pixel layout (572 x 300)
   as percentages, so it scales with the column instead of cropping.
   ====================================================================== */
.article-hero {
  position: relative;
  width: 100%;
  aspect-ratio: 572 / 300;
  border-radius: 10px;
  overflow: hidden;
  isolation: isolate;
  background: linear-gradient(180deg, #fdcc3d 0%, #fddc7c 100%);
}
.article-hero img {
  position: absolute;
  max-width: none;
  object-fit: cover;
  display: block;
  pointer-events: none;
  user-select: none;
}
.article-hero-overlay { mix-blend-mode: soft-light; }
.article-hero-a { left: -9.79%;  top: 0;       width: 56.99%; height: 109%; }
.article-hero-b { left: -4.72%;  top: -1.67%;  width: 98.78%; height: 94.33%; }
.article-hero-c { left: 61.54%;  top: -15.33%; width: 56.64%; height: 137.67%; }
.article-hero-d { left: 37.41%;  top: 23%;     width: 69.23%; height: 154%; }
.article-hero-main { left: 16.96%; top: 10%;   width: 78.32%; height: 93.33%; }

/* ======================================================================
   Earlier work
   A role row with one line under it. The note sits on the row's text
   column, and a row with nowhere to go (Volla) keeps its colour still.
   ====================================================================== */
.row-note {
  color: var(--text);
  margin: -2px 0 10px;
}
.row-note:last-child { margin-bottom: 0; }
.link-row-static .row-name { color: var(--text-strong); }

/* ======================================================================
   Resume
   The same page doubles as the PDF: print hides the site around it and
   sets it as a document. resume.pdf is rendered from this page
   (scripts/resume/pdf.mjs), so the two can't drift apart.
   ====================================================================== */
.cv-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; width: 100%; }
.cv-title h1 { font-size: 24px; line-height: 1.2; font-weight: 500; letter-spacing: -0.01em; color: var(--text-strong); }
.cv-sub { margin-top: 4px; color: var(--text-muted); }
.cv-download { flex-shrink: 0; }
.cv-download svg { width: 14px; height: 14px; }
a.cv-download { padding-left: 8px; }

.cv-contact { display: flex; flex-wrap: wrap; gap: 4px 8px; color: var(--text-muted); margin-top: -18px; }

.cv-section { display: flex; flex-direction: column; gap: 12px; width: 100%; }
.cv-section .study-label { font-size: var(--font); font-weight: 400; }
.cv-body { display: flex; flex-direction: column; gap: 18px; color: var(--text-strong); }
.cv-item { display: flex; flex-direction: column; gap: 6px; }
.cv-role { display: block; text-decoration: none; color: inherit; }
.cv-role .row-name { font-weight: 500; }
a.cv-role:hover .row-name { box-shadow: inset 0 -1px 0 0 currentColor; }
.cv-item ul { padding-left: 18px; display: flex; flex-direction: column; gap: 4px; color: var(--text); }
.cv-code { font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: 0.9em; }
.cv-skills { gap: 6px; }
.cv-skills p { color: var(--text); }
.cv-skills span { display: inline-block; min-width: 64px; color: var(--text-muted); }

@media (max-width: 600px) {
  .cv-head { flex-direction: column; align-items: flex-start; }
}

@media print {
  @page { size: A4; margin: 13mm 16mm; }
  html, html[data-theme="dark"] {
    --page: #fff; --text-strong: #111; --text: #3a3a3a; --text-muted: #6b6b6b; --rule: rgba(0,0,0,.12);
    color-scheme: light;
  }
  body { background: #fff; font-size: 9pt; line-height: 1.4; transition: none; }
  .chrome, .nav, .crumb, .player, footer, .stickers, .sound-note, .cv-download, .gallery { display: none !important; }
  .appear { opacity: 1 !important; animation: none !important; transform: none !important; }
  .content { width: auto; max-width: none; padding: 0; gap: 12px; }
  .cv-title h1 { font-size: 20pt; }
  .cv-contact { margin-top: -10px; }
  .cv-contact a { text-decoration: none; }
  .cv-item { break-inside: avoid; }
  .cv-section { gap: 6px; }
  .cv-section .study-label { break-after: avoid; }
  .cv-body { gap: 8px; }
  .cv-item { gap: 3px; }
  .cv-item ul { gap: 1px; }
  .cv-skills { gap: 2px; }
}

/* ======================================================================
   Card to case study
   Each work card on the homepage and the hero on its case study share a
   view-transition-name, so following the card grows it into the hero, and
   going back shrinks the hero into the card. Cross-document, no JS; browsers
   without support just navigate.
   ====================================================================== */
@view-transition { navigation: auto; }

.cards .card-luotain,   .study-hero.card-luotain   { view-transition-name: work-luotain; }
.cards .card-costgraph, .study-hero.card-costgraph { view-transition-name: work-costgraph; }
.cards .card-yote,      .study-hero.card-yote      { view-transition-name: work-yote; }
.cards .card-lonar,     .study-hero.card-lonar     { view-transition-name: work-lonar; }
.cards .card-kernui,    .study-hero.card-kernui    { view-transition-name: work-kernui; }

::view-transition-group(work-luotain),
::view-transition-group(work-costgraph),
::view-transition-group(work-yote),
::view-transition-group(work-lonar),
::view-transition-group(work-kernui) {
  /* Navigation is something people do again and again: quick, not a show */
  animation-duration: 0.38s;
  animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
}
/* No object-fit here. The card is portrait and the hero is wide, and
   cover-scaling either one while the box changes shape makes the zoom rise
   and fall mid-flight, which reads as a shake. At their own aspect, pinned
   top and clipped by the pair, both scale in one direction only. */
::view-transition-image-pair(work-luotain),
::view-transition-image-pair(work-costgraph),
::view-transition-image-pair(work-yote),
::view-transition-image-pair(work-lonar),
::view-transition-image-pair(work-kernui) {
  overflow: hidden;          /* the taller snapshot is cut at the box, not spilled */
  border-radius: 10px;
}
::view-transition-old(root),
::view-transition-new(root) { animation-duration: 0.24s; }

html.vt-in .study-head.appear,
html.vt-in .cards-section.appear { animation: none; opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  @view-transition { navigation: none; }
}

/* ======================================================================
   Framed hero
   The case study's cover: the project's mark and name, one line, and the
   product rising from the bottom edge, cut off by the card, on the same
   gradient as its homepage card.
   ====================================================================== */
.study-hero-framed {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-start;
  padding: 6% 0 0;
  text-align: center;
  color: #fff;
}
.study-hero-framed .hero-brand {
  position: relative;
  z-index: 2;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font-size: 15px;
  line-height: 1;
  font-weight: 500;
  letter-spacing: -0.01em;
}
.study-hero-framed .hero-brand img {
  width: 18px;
  height: 18px;
  object-fit: contain;
  border-radius: 0;
  box-shadow: none;
  filter: brightness(0) invert(1);
}
.study-hero-framed .hero-line {
  position: relative;
  z-index: 2;
  margin-top: 3.2%;
  max-width: 72%;
  font-size: clamp(16px, 3.7vw, 21px);
  line-height: 1.15;
  font-weight: 500;
  letter-spacing: -0.025em;
  text-wrap: balance;
  text-shadow: 0 1px 12px rgb(0 0 0 / 0.12);
}
/* The shot is wider than the space left and runs off the bottom edge */
.study-hero-framed .hero-shot {
  flex: none;
  width: 80%;
  height: auto;
  max-height: none;
  margin-top: 5.5%;
  background: #fff;
  object-fit: cover;
  object-position: top center;
  border-radius: 8px 8px 0 0;
  box-shadow: 0 18px 50px -12px rgb(0 0 0 / 0.45), 0 0 0 1px rgb(255 255 255 / 0.12);
}

@media (max-width: 600px) {
  .study-hero-framed { aspect-ratio: 4 / 3.4; padding: 8% 0 0; }
  .study-hero-framed .hero-brand { font-size: 13px; }
  .study-hero-framed .hero-brand img { width: 15px; height: 15px; }
  .study-hero-framed .hero-shot { width: 86%; margin-top: 6%; }
}

/* ======================================================================
   Case-study reading size
   The site's 13.5px works for a homepage of short lines; a 1,000-word case
   study read on a phone needs more. Labels, pills and captions stay small.
   ====================================================================== */
.study-content { font-size: 15px; line-height: 24px; }
.study-content .study-subhead { font-size: inherit; line-height: inherit; }
.study-lede { font-size: 15px; line-height: 23px; }

/* ======================================================================
   Work row
   The row starts on the text column at every width and runs off the right
   edge. Two step buttons sit beside the label at every width, since the
   scrollbar is hidden and a wheel only scrolls down; they grey out at
   either end, and when a very wide screen fits all five.
   ====================================================================== */
.cards-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
.cards-head .cards-label { margin-bottom: 0; }
.cards-nav { display: flex; gap: 2px; margin-right: -6px; }
.cards-nav[hidden] { display: none; }
.cards-step {
  width: 26px; height: 26px;
  display: grid; place-items: center;
  border: none; border-radius: 6px;
  background: transparent;
  color: var(--text-strong);
  cursor: pointer;
  transition: background-color 0.15s ease, opacity 0.15s ease;
}
.cards-step svg { width: 14px; height: 14px; display: block; }
.cards-step:hover:not(:disabled), .cards-step:focus-visible { background: var(--hover); outline: none; }
.cards-step:disabled { opacity: 0.28; cursor: default; }
.cards { scroll-behavior: smooth; scroll-snap-type: x proximity; }
.cards .card { scroll-snap-align: start; scroll-margin-inline-start: max(24px, calc(50vw - 270px)); }
@media (prefers-reduced-motion: reduce) { .cards { scroll-behavior: auto; } }
