@font-face {
  font-family: 'Inter';
  src: url('../fonts/Inter-Variable.woff2') format('woff2');
  font-weight: 400 800;
  font-style: normal;
  font-display: swap;
}

/* Test font for .hero-name only - see fonts/Metropolitano-*.ttf. */
@font-face {
  font-family: 'Metropolitano';
  src: url('../fonts/Metropolitano-Medium.ttf') format('truetype');
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

:root {
  --bg: #1c1c1c;
  --bg-alt: #292929;
  --bg-section: #202020;
  /* Header: darker than --bg (rather than a notch below it), because the
     grain overlay (::before, below) sits on top and its own alpha channel
     (not just --grain-opacity) lets a fair amount of its bright noise
     through, and the header's 0.85 alpha lets ~15% of whatever's blurred
     behind it through too. Both push the flat colour up, so the base has to
     start darker for the result to still land on --bg-header-solid (the
     footer's flat colour, with neither effect) - values solved empirically
     by compositing the actual grain tile in a canvas, not estimated from
     its RGB average, since the noise's own alpha varies per pixel. The
     translucent value is what the blur samples through; the solid one is
     that same colour already composited over the page, for the mobile nav
     panel (no backdrop behind it to blur, and no grain there either). */
  --bg-header: rgba(12, 12, 12, 0.85);
  --bg-header-solid: #171717;
  /* Same grain-compensation as --bg-header above, but for the flat (non
     -translucent) grained section panels - solved so the grain-composited
     result lands back on --bg-header-solid's #171717. */
  --bg-panel: #0e0e0e;
  --border: #363636;
  --text: #f2f2f2;
  --text-muted: #9a9a9a;
  --accent: #E8A93B;
  --header-h: 72px;
  --max-w: 1920px;
  /* Project pages read as a contained, compact page (bordered column) rather
     than full-bleed - a video/image can otherwise get huge on a wide
     monitor. Narrower than --max-w on purpose. */
  --project-max-w: 1300px;
  --edge: 72px;
  --grain: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxODAiIGhlaWdodD0iMTgwIj4KICA8ZmlsdGVyIGlkPSJuIj4KICAgIDxmZVR1cmJ1bGVuY2UgdHlwZT0iZnJhY3RhbE5vaXNlIiBiYXNlRnJlcXVlbmN5PSIwLjkiIG51bU9jdGF2ZXM9IjMiIHN0aXRjaFRpbGVzPSJzdGl0Y2giLz4KICAgIDxmZUNvbG9yTWF0cml4IHR5cGU9InNhdHVyYXRlIiB2YWx1ZXM9IjAiLz4KICA8L2ZpbHRlcj4KICA8cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWx0ZXI9InVybCgjbikiLz4KPC9zdmc+");
  --grain-opacity: 0.1;
  /* Already-dark tech-grid texture, used as-is (no mask/tint) - just tiled
     over --bg at partial opacity. Swap the url() for a different tile. */
  --motif: url("../assets/misc/bg_motif7.jpg");
  --motif-size: 620px;
  --motif-opacity: 0.6;

  /* Page-transition timing — tweak these two to retune the effect everywhere. */
  --page-anim-duration: 1.4s;
  --page-anim-ease: cubic-bezier(0.65, 0, 0.07, 1); /* ease-in-out-expo: soft start, soft landing, snaps in the middle */
  --page-fade-ease: linear; /* the outgoing page's dissolve — steady, so it doesn't snap like the motion does */
}

/* Light theme - a straight colour inversion for now (dark/white swap,
   accent yellow -> teal), not a from-scratch light palette. Toggled via
   [data-theme] on <html>, set by js/theme.js.
   --motif swaps to a light recolour of the same tile (bg_motif7_white.jpg -
   light-grey line art on white, vs. the dark tile's white-on-black) so it
   reads the same way dark mode's does instead of washing out. --grain (a
   light noise texture, avg. ~187,187,187 at --grain-opacity) still runs
   everywhere same as dark mode, and several panels layer it a SECOND time
   locally (.site-header::before, .selected-works::before,
   .about-block::before) on top of their own background-color - same as
   dark mode's --bg-panel/--bg-header, those inputs are deliberately NOT the
   final visible colour, but solved so the colour AFTER that local grain
   blend lands on the intended flat grey (~#efefef in, ~#eaeaea out -
   measured via actual canvas compositing of the real assets, not
   estimated). --bg-header-solid (footer, mobile nav - no local grain
   there) is set directly to that same finished tone. General page bg
   (grain only, no local second pass) needs no such input offset - #fff
   composites to ~#f8f8f8, close enough to read as white. */
:root[data-theme="light"] {
  --bg: #ffffff;
  --bg-alt: #f0f0f0;
  --bg-section: #f0f0f0;
  --bg-header: rgba(246, 246, 246, 0.92);
  --bg-header-solid: #f0f0f0;
  --bg-panel: #f6f6f6;
  --border: #cccccc;
  --text: #1a1a1a;
  --text-muted: #6b6b6b;
  --accent: #B8722E;
  --motif: url("../assets/misc/bg_motif7_white.jpg");
  --motif-opacity: 0.6;
}

html {
  height: 100%;
  background-color: var(--bg);
  scrollbar-gutter: stable;
}

body {
  position: relative;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  /* No background-color here on purpose: the page background is html's (which
     paints the canvas). An opaque background on <body> would paint over
     body::before below, which sits at z-index -1 — that is how the global
     grain ended up invisible everywhere except inside a transition layer. */
  color: var(--text);
  font-family: 'Inter', 'Helvetica Neue', Arial, sans-serif;
  font-weight: 400;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

body::before {
  content: '';
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  opacity: var(--grain-opacity);
  background-image: var(--grain);
  background-repeat: repeat;
  background-size: 180px 180px;
}

body::after {
  content: '';
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  opacity: var(--motif-opacity);
  background-image: var(--motif);
  background-repeat: repeat;
  background-size: var(--motif-size);
}

a {
  color: inherit;
}

img {
  max-width: 100%;
  display: block;
}

a:focus-visible,
button:focus-visible,
input:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

/* ---------- Page transitions ---------- */
/* Outgoing page rises and fades out, incoming page rises in from below.
   Both share the --page-anim-ease curve (see :root above).
   Two mechanisms use the same keyframes:
   - ::view-transition-*(root) for browsers that support cross-document
     View Transitions natively (progressive enhancement, no JS).
   - .page-transition-layer, driven by js/router.js, for every browser —
     it fetches the next page and animates the swap by hand. */
/* Transform-based: used by the native cross-document View Transitions API,
   which is spec'd around transform/opacity on browser-managed snapshots. */
@keyframes page-transition-exit {
  from {
    transform: translateY(0);
    opacity: 1;
  }
  to {
    transform: translateY(-12vh);
    opacity: 0;
  }
}

@keyframes page-transition-enter {
  from {
    transform: translateY(100vh);
  }
  to {
    transform: translateY(0);
  }
}

@media not (prefers-reduced-motion: reduce) {
  ::view-transition-old(root) {
    animation: page-transition-exit var(--page-anim-duration) var(--page-anim-ease) both;
  }

  ::view-transition-new(root) {
    animation: page-transition-enter var(--page-anim-duration) var(--page-anim-ease) both;
  }
}

/* Margin-based: used by the manual JS router (.page-transition-layer).
   Deliberately NOT transform/will-change — this keeps the animation off the
   GPU compositor entirely, which is what a transform-driven version kept
   rendering with a brief but reproducible washed-out/lighter look. */
@keyframes page-transition-exit-margin {
  from {
    margin-top: 0;
  }
  to {
    margin-top: -12vh;
  }
}

@keyframes page-transition-enter-margin {
  from {
    margin-top: 100vh;
  }
  to {
    margin-top: 0;
  }
}

.page-transition-layer {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 100vh;
  z-index: 40;
  overflow: hidden;
  pointer-events: none;
  background-color: var(--bg);
  /* Makes the layer the containing block for position: fixed descendants
     (.hero-pin). Without it the home hero anchors to the viewport instead of
     the layer, so it appears instantly at its resting spot, unclipped and
     without its background, while the outgoing page slides away behind it.
     contain: paint is used rather than a transform, which would promote the
     layer to the compositor — the thing the animation above avoids. */
  contain: paint;
}

.page-transition-frozen {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}

@keyframes page-transition-fade-out {
  from {
    opacity: 1;
  }
  to {
    opacity: 0;
  }
}

/* The layer hides body::before, so it repaints the grain itself. z-index: -1 is
   what keeps it *behind* the content, matching the rest of the site — the
   earlier version had no z-index, which painted grain on top of everything and
   read as the background brightening for the length of the transition. */
.page-transition-layer::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  opacity: var(--grain-opacity);
  background-image: var(--grain);
  background-repeat: repeat;
  background-size: 180px 180px;
}

/* Same reasoning as body::before/::after above: the layer hides body's
   pseudo-elements during a transition, so it needs its own copy of the
   motif texture too, or the pattern would flash off mid-animation. */
.page-transition-layer::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  opacity: var(--motif-opacity);
  background-image: var(--motif);
  background-repeat: repeat;
  background-size: var(--motif-size);
}

@media not (prefers-reduced-motion: reduce) {
  .page-transition-layer.page-anim-out {
    animation: page-transition-exit-margin var(--page-anim-duration) var(--page-anim-ease) both;
  }

  .page-transition-layer.page-anim-out .page-transition-frozen {
    animation: page-transition-fade-out var(--page-anim-duration) var(--page-fade-ease) both;
  }

  .page-transition-layer.page-anim-in {
    animation: page-transition-enter-margin var(--page-anim-duration) var(--page-anim-ease) both;
  }
}

/* ---------- Header ---------- */
.site-header {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: var(--header-h);
  display: flex;
  align-items: center;
  z-index: 100;
  background-color: var(--bg-header);
  backdrop-filter: blur(8px);
}

/* Reuses .wrap's max-width/margin/padding (same trick as .hero-inner
   further down) so the logo/nav/actions line up with every other section's
   left/right edge - .site-header itself stays full-bleed for the
   background/blur/border. */
.header-inner {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  width: 100%;
}

.site-header::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  opacity: var(--grain-opacity);
  background-image: var(--grain);
  background-repeat: repeat;
  background-size: 180px 180px;
}

.site-header .logo {
  justify-self: start;
  font-weight: 800;
  letter-spacing: -0.04em;
  text-decoration: none;
  font-size: 1.1rem;
  text-transform: uppercase;
  color: var(--text-muted);
}

.main-nav {
  justify-self: center;
  display: flex;
  gap: 18px;
}

.main-nav a {
  text-decoration: none;
  color: var(--text-muted);
  font-size: 1.08rem;
  text-transform: uppercase;
  letter-spacing: -0.04em;
  transition: color 0.2s ease;
  position: relative;
  padding-bottom: 4px;
}

.main-nav a:hover,
.main-nav a.is-active {
  color: var(--text);
}

.main-nav a::after {
  content: '';
  position: absolute;
  left: 0;
  bottom: -2px;
  height: 2px;
  width: 0;
  background: var(--accent);
  transition: width 0.35s cubic-bezier(0.22, 1, 0.36, 1);
}

.main-nav a:hover::after,
.main-nav a.is-active::after {
  width: 100%;
}

.header-actions {
  justify-self: end;
  display: flex;
  align-items: center;
  gap: 22px;
}

/* Dark/Light label pair, styled like a compact nav toggle - the current
   theme reads bright, the other one dim, and clicking flips which is which. */
.theme-toggle {
  background: none;
  border: none;
  padding: 0;
  font-family: inherit;
  font-size: 0.85rem;
  letter-spacing: 0.02em;
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 4px;
}

.theme-toggle-option {
  color: var(--text-muted);
  transition: color 0.2s ease;
}

.theme-toggle:hover .theme-toggle-option {
  color: var(--text);
}

.theme-toggle .theme-toggle-option.is-active {
  color: var(--accent);
}

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

.nav-toggle {
  display: none;
  flex-direction: column;
  justify-content: center;
  gap: 5px;
  width: 32px;
  height: 32px;
  background: none;
  border: none;
  cursor: pointer;
  z-index: 110;
}

.nav-toggle span {
  display: block;
  height: 2px;
  background: var(--text);
  transition: transform 0.25s ease, opacity 0.25s ease;
}

/* ---------- Main layout ---------- */
main {
  padding-top: var(--header-h);
  flex: 1 0 auto;
}

.wrap {
  max-width: var(--max-w);
  margin: 0 auto;
  padding: 0 var(--edge);
}

/* ---------- Hero ---------- */
.hero-scroll-space {
  position: relative;
  height: 100vh;
}

/* Previous layout: name + role centred as one block. Kept for an easy
   revert - swap back in and drop the top-left/bottom-right rules below.
.hero-pin {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 100vh;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 60px var(--edge);
  filter: blur(calc(var(--p, 0) * 14px));
  transform: translateY(40px) scale(calc(1 - var(--p, 0) * 0.12));
  opacity: calc(1 - var(--p, 0));
}

.hero-name {
  font-weight: 480;
  letter-spacing: -0.04em;
  line-height: 1.02;
  font-size: clamp(2.8rem, 9vw, 7.8rem);
  text-transform: uppercase;
}

.hero-role {
  color: var(--accent);
  font-size: clamp(0.9rem, 1.8vw, 1.2rem);
  margin-top: 22px;
  letter-spacing: -0.04em;
  text-transform: uppercase;
}
*/

.hero-pin {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 100vh;
  z-index: 1;
  padding: 100px 0;
  filter: blur(calc(var(--p, 0) * 14px));
  transform: translateY(40px);
  opacity: calc(1 - var(--p, 0));
}

/* Reuses .wrap's max-width/margin/padding so the hero's left/right edges
   match every other section (Selected Works, etc.) exactly, even past
   --max-w on very wide screens - .hero-pin itself stays full-bleed (it needs
   to for the fixed positioning/backdrop-filter glow) and only this inner
   layer is constrained. */
.hero-inner {
  height: 95%;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
}

.hero-top {
  width: fit-content;
  align-self: flex-start;
  margin-top: 20px;
}

/* Glow behind the hero name: rather than a flat colour blob (or a second
   copy of the motif image, which drifted out of sync with the real one on
   body::after the moment .hero-pin's own scroll transform kicked in),
   backdrop-filter brightens whatever is actually rendered behind this layer
   - the real, single instance of the texture - so it can't desync. Radially
   masked so it reads as a soft light in the middle. Lives on its own layer
   so the mask/filter never touch the h1/p text, which are real .hero-pin
   children. */
.hero-pin::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  backdrop-filter: brightness(1.9);
  -webkit-backdrop-filter: brightness(1.9);
  -webkit-mask-image: radial-gradient(ellipse 55% 50% at 50% 42%, black 0%, transparent 80%);
  mask-image: radial-gradient(ellipse 55% 50% at 50% 42%, black 0%, transparent 80%);
}

.hero-name {
  /* Metropolitano Medium (local font, see @font-face above) - also used
     for every other big title on the site: .page-title, .section-title,
     .project-title. */
  font-family: 'Metropolitano', 'Inter', 'Helvetica Neue', Arial, sans-serif;
  font-weight: 500;
  letter-spacing: -0.06em;
  line-height: 0.8;
  font-size: clamp(3.2rem, 11vw, 11rem);
  text-transform: uppercase;
  text-align: left;
}

.hero-name-line {
  display: block;
}

/* The "+" reads small next to the capital letters at this size - bump just
   that first character up. line-height needs pinning too, otherwise the
   bigger glyph's normal line-height inflates just this line's height and
   pushes it away from the line above. */
.hero-name-line--second::first-letter {
  font-size: 1.2em;
  line-height: 0.9;
}

.hero-role {
  color: var(--accent);
  font-size: clamp(0.9rem, 1.8vw, 1.2rem);
  margin-top: 8px;
  letter-spacing: -0.04em;
  text-transform: uppercase;
  text-align: right;
}

.hero-bottom {
  width: 100%;
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 24px;
}

.hero-scroll-cue {
  flex-shrink: 0;
  display: block;
  width: 44px;
  height: 44px;
  margin-bottom: 30px;
  color: var(--text);
  transition: color 0.2s ease;
}

.hero-scroll-cue svg {
  width: 100%;
  height: 100%;
}

.hero-scroll-cue:hover {
  color: var(--accent);
}

@media not (prefers-reduced-motion: reduce) {
  .hero-scroll-cue {
    animation: hero-scroll-cue-bounce 2.2s ease-in-out infinite;
  }
}

@keyframes hero-scroll-cue-bounce {
  0%, 100% {
    transform: translateY(0);
  }
  50% {
    transform: translateY(8px);
  }
}

.hero-bio {
  max-width: 630px;
  color: var(--text);
  font-size: 2.0rem;
  font-weight: 0;
  line-height: 1.05;
  letter-spacing: -0.04em;
  text-align: justify;
  text-indent: 140px;
} 

/* ---------- Sections ---------- */
.section {
  padding: 70px 0 100px;
}

.selected-works {
  position: relative;
  z-index: 2;
  background-color: var(--bg-panel);
}

.selected-works::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  opacity: var(--grain-opacity);
  background-image: var(--grain);
  background-repeat: repeat;
  background-size: 180px 180px;
}

.section-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 24px;
  margin-bottom: 16px;
}

.section-head .section-title {
  margin-bottom: 0;
}

.see-all {
  flex-shrink: 0;
  text-decoration: none;
  text-transform: uppercase;
  letter-spacing: 0.02em;
  font-size: 0.9rem;
  color: var(--text-muted);
  border-bottom: 1px solid var(--text-muted);
  padding-bottom: 2px;
  transition: color 0.2s ease, border-color 0.2s ease;
}

.see-all:hover {
  color: var(--text);
  border-color: var(--accent);
}

.section-title {
  font-family: 'Metropolitano', 'Inter', 'Helvetica Neue', Arial, sans-serif;
  font-size: clamp(2.2rem, 5vw, 3.5rem);
  font-weight: 500;
  margin-bottom: 4px;
  letter-spacing: -0.04em;
  text-transform: uppercase;
}

/* ---------- Grid / cards ---------- */
.grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 30px;
}

.card {
  text-decoration: none;
  color: inherit;
  display: block;
}

.card-thumb {
  aspect-ratio: 16 / 10;
  overflow: hidden;
  background: var(--bg-alt);
  border: 1px solid var(--border);
}

.card-thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  filter: saturate(0.8) contrast(0.94) brightness(0.95);
  transition: transform 0.7s cubic-bezier(0.22, 1, 0.36, 1),
              filter 0.7s cubic-bezier(0.22, 1, 0.36, 1);
}

.card:hover .card-thumb img {
  transform: scale(1.03);
  filter: saturate(1) contrast(1) brightness(1);
}

.card-title {
  margin-top: 16px;
  font-weight: 400;
  font-size: 1.02rem;
  text-transform: uppercase;
}

.card-meta {
  color: var(--text-muted);
  font-size: 0.95rem;
  margin-top: 4px;
}

.card-meta .meta-sep {
  color: var(--accent);
}

.card-video {
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  text-align: left;
  cursor: pointer;
  width: 100%;
}

.card-video .card-thumb {
  position: relative;
}

/* Marks a note rendered into a project grid (Works > Video Games, see
   renderNoteAsCard in js/note-card.js) as a blog post rather than a
   project - card-thumb isn't position:relative by default, so this rule
   also opts it in just for that case. */
.card-thumb:has(.card-badge) {
  position: relative;
}

.card-badge {
  position: absolute;
  top: 12px;
  left: 12px;
  z-index: 1;
  padding: 4px 10px;
  background: var(--bg);
  color: var(--text);
  font-size: 0.72rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  border: 1px solid var(--border);
}

.card-video .card-thumb::after {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: 56px;
  height: 56px;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.55);
  border: 1px solid rgba(255, 255, 255, 0.4);
  transition: transform 0.3s ease, background 0.3s ease, border-color 0.3s ease;
}

.card-video .card-thumb::before {
  content: '';
  position: absolute;
  top: 50%;
  left: calc(50% + 3px);
  width: 0;
  height: 0;
  border-style: solid;
  border-width: 9px 0 9px 15px;
  /* Fixed white, not var(--text) - the circle behind it is always a dark,
     semi-transparent scrim regardless of theme, so this needs to stay
     light to read against it in both dark and light mode. */
  border-color: transparent transparent transparent #f2f2f2;
  transform: translate(-50%, -50%);
  z-index: 1;
}

.card-video:hover .card-thumb::after {
  background: var(--accent);
  border-color: var(--accent);
  transform: translate(-50%, -50%) scale(1.08);
}

/* ---------- About ---------- */
.about-layout {
  display: grid;
  grid-template-columns: 1.3fr 0.7fr;
  gap: 60px;
  align-items: start;
  margin-top: 36px;
}

.about-grid {
  display: grid;
  gap: 24px;
}

.about-grid p {
  color: var(--text-muted);
  font-size: 1.10rem;
  text-align: justify;
}

.about-portrait {
  width: 100%;
  max-width: 400px;
  aspect-ratio: 1 / 1;
  border-radius: 50%;
  overflow: hidden;
  justify-self: center;
}

.about-portrait img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transform: scaleX(-1);
}

/* ---------- Contact ---------- */

.contact-block .wrap {
  display: flex;
  flex-direction: column;
  gap: 24px;
}

/* Alternating section background (Selected Works / About Me get the grain
   -textured panel - same flat colour as the footer once the grain is
   blended in, see --bg-panel above; Featured Posts / Contact stay on the
   base bg). */
.about-block {
  position: relative;
  z-index: 0;
  background-color: var(--bg-panel);
}

.about-block::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  opacity: var(--grain-opacity);
  background-image: var(--grain);
  background-repeat: repeat;
  background-size: 180px 180px;
}

.contact-links {
  display: flex;
  flex-wrap: wrap;
  gap: 20px;
}

.contact-links a {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 56px;
  height: 56px;
  border-radius: 50%;
  border: 1px solid var(--border);
  color: var(--text);
  transition: color 0.2s ease, border-color 0.2s ease;
}

.contact-links a svg {
  width: 26px;
  height: 26px;
}

.contact-links a:hover {
  color: var(--accent);
  border-color: var(--accent);
}

/* ---------- Scroll reveal ---------- */
.reveal {
  opacity: 0;
  transform: translateY(28px);
  transition: opacity 0.7s cubic-bezier(0.22, 1, 0.36, 1),
              transform 0.7s cubic-bezier(0.22, 1, 0.36, 1);
}

.reveal.is-visible {
  opacity: 1;
  transform: translateY(0);
}

.reveal-stagger.is-visible {
  transition-delay: calc(var(--stagger-i, 0) * 60ms);
}

/* Section-level entrance on the home page: a plain opacity/scale "pop"
   rather than the slide-up used for individual cards/items, so whole
   sections don't travel vertically as they arrive. */
.reveal-pop {
  transform: scale(0.97);
  transition: opacity 0.6s cubic-bezier(0.22, 1, 0.36, 1),
              transform 0.6s cubic-bezier(0.22, 1, 0.36, 1);
}

.reveal-pop.is-visible {
  transform: scale(1);
}

/* ---------- Generic page header/content ---------- */
.page-header {
  padding: 70px var(--edge) 0;
}

.page-content {
  padding-bottom: 120px;
}

/* ---------- Works page ---------- */
.page-title {
  font-family: 'Metropolitano', 'Inter', 'Helvetica Neue', Arial, sans-serif;
  font-size: clamp(2.6rem, 7vw, 4.6rem);
  font-weight: 500;
  letter-spacing: -0.04em;
  text-transform: uppercase;
}

.section-tabs {
  display: flex;
  flex-wrap: wrap;
  gap: 28px;
  margin: 24px 0 20px;
  padding-bottom: 8px;
}

/* Short rule under the category row only (Animation/Video Games) - shrunk
   to that row's own content width via fit-content, rather than a
   page-width line, so it reads as a small divider before the subsection
   row rather than a full section border. */
.section-tabs:not(.section-subtabs) {
  width: fit-content;
}

/* Only when a subsection row actually follows (e.g. Animation, not Video
   Games) - otherwise there's nothing to separate the category row from. */
.section-tabs:not(.section-subtabs):has(+ .section-subtabs:not([hidden])) {
  padding-bottom: 12px;
  border-bottom: 1px solid var(--border);
}

.section-tabs button {
  background: none;
  border: none;
  color: var(--text-muted);
  font-family: inherit;
  font-size: 1.22rem;
  text-transform: uppercase;
  font-weight: 100;
  letter-spacing: -0.04em;
  cursor: pointer;
  padding: 4px 0;
  position: relative;
}

.section-tabs button:hover,
.section-tabs button.is-active {
  color: var(--text);
}

/* Same grow-from-left underline as .main-nav a — width animates in on
   hover, and stays full-width while the tab is the active section. */
.section-tabs button::after {
  content: '';
  position: absolute;
  left: 0;
  bottom: -2px;
  height: 2px;
  width: 0;
  background: var(--accent);
  transition: width 0.35s cubic-bezier(0.22, 1, 0.36, 1);
}

.section-tabs button:hover::after,
.section-tabs button.is-active::after {
  width: 100%;
}

/* Subsection row (e.g. Commercials/Short Films/... under Animation) sits
   right under the category row with its own, tighter spacing — it replaces
   the category row's bottom margin rather than stacking under it. */
.section-subtabs {
  margin-top: -14px;
}

.section-subtabs button {
  font-size: 1.04rem;
}

.section-subtabs[hidden] {
  display: none;
}

/* ---------- Private / Secret Stash ---------- */
/* Small kicker linking back to the stash landing - sits above .page-title
   or .project-title on every page inside /private, same eyebrow treatment
   as .project-sections so it reads as part of the same title block rather
   than a bolted-on banner. */
.stash-eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  color: var(--text-muted);
  text-decoration: none;
  font-size: 1.08rem;
  font-weight: 500;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  margin-bottom: 12px;
  transition: color 0.2s ease;
}

.stash-eyebrow:hover {
  color: var(--accent);
}

.stash-eyebrow svg {
  width: 18px;
  height: 18px;
  flex-shrink: 0;
}

/* On case-study pages the eyebrow sits next to the project's own category
   (e.g. "The Secret Stash • 3D Craft") on one line instead of stacking. */
.stash-crumb {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px;
  margin-bottom: 10px;
}

.stash-crumb .stash-eyebrow {
  margin-bottom: 0;
}

.stash-crumb .project-sections {
  margin: 0;
  font-size: 1.08rem;
  text-transform: uppercase;
}

.stash-crumb .project-sections::before {
  content: '•';
  color: var(--accent);
  margin-right: 10px;
}

/* Landing page h1 - same size/weight as every other .page-title, the chest
   just sits inline before the text instead of a plain word. */
.stash-title {
  display: flex;
  align-items: center;
  gap: 0.14em;
}

.stash-title svg {
  width: 0.95em;
  height: 0.95em;
  flex-shrink: 0;
  color: var(--accent);
}

.stash-tagline {
  color: var(--text-muted);
  font-size: 1.1rem;
  margin-top: 14px;
  margin-bottom: 36px;
  white-space: nowrap;
}

.category-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 24px;
}

.category-card {
  display: block;
  padding: 32px 28px;
  border: 1px solid var(--border);
  background: var(--bg-alt);
  text-decoration: none;
  color: inherit;
  transition: border-color 0.2s ease, transform 0.3s cubic-bezier(0.22, 1, 0.36, 1);
}

.category-card:hover {
  border-color: var(--accent);
  transform: translateY(-4px);
}

.category-card-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
}

.category-card-title {
  font-family: 'Metropolitano', 'Inter', 'Helvetica Neue', Arial, sans-serif;
  font-size: 1.4rem;
  font-weight: 500;
  letter-spacing: -0.02em;
  text-transform: uppercase;
  padding-top: 6px;
}

.category-card-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 56px;
  height: 56px;
  flex-shrink: 0;
  border-radius: 50%;
  border: 1px solid var(--border);
  color: var(--accent);
  transition: border-color 0.2s ease;
}

.category-card:hover .category-card-icon {
  border-color: var(--accent);
}

.category-card-icon svg {
  width: 26px;
  height: 26px;
}

.category-card-desc {
  margin-top: 20px;
  color: var(--text-muted);
  font-size: 0.95rem;
  line-height: 1.4;
}

.category-card-lock {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin-top: 18px;
  color: var(--text-muted);
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.category-card-lock svg {
  width: 13px;
  height: 13px;
}

/* ---------- Project page ---------- */
/* Keeps the whole page (header, hero, context, blocks - everything is a
   child of this single element) a compact column instead of the site's
   usual full-bleed width - narrowing the outer box is enough: every
   child's own max-width (.wrap, .blocks) can no longer exceed it, so
   nothing inside needs its own edit. Tune --project-max-w above (:root) to
   change the column width. Same treatment on the note page (its own root
   wrapper - see note.html). Blog and contact opt back out below - their
   own content (.notes-list, .contact-layout) already governs its width, so
   those two roots just match the site's usual full --max-w instead. */
[data-project-root],
[data-note-root] {
  max-width: var(--project-max-w);
  margin: 0 auto;
}

.project-header {
  /* Vertical padding only — the inner .wrap (see project.html) already
     applies var(--edge) horizontally + centers via max-width. Padding it
     here too used to double the left/right offset vs. .blocks below,
     which applies --edge just once. Bottom padding matches .section-title's
     margin-bottom on the home page, so the title sits the same distance
     from the hero media here as a home section's title sits from its
     content below. */
  padding: 90px 0 4px;
}

/* Which section(s) (e.g. Commercials, Short Films) this project belongs to,
   as a small eyebrow row above the title - the bullet only separates
   multiple labels, it isn't a bullet-point marker, so a single label gets
   none. */
.project-sections {
  color: var(--text-muted);
  font-weight: 300;
  font-size: 1.15rem;
  letter-spacing: -0.01em;
  line-height: 1;
  margin-left: 5px;
  margin-bottom: -12px;
}

.project-sections .section-dot {
  color: var(--accent);
}

/* Same look as .page-title (Works/Blog/Contact...) - a project or note page
   is content within that same section, not a distinct heading style. */
.project-title {
  font-family: 'Metropolitano', 'Inter', 'Helvetica Neue', Arial, sans-serif;
  font-size: clamp(2.6rem, 7vw, 4.6rem);
  font-weight: 500;
  letter-spacing: -0.04em;
  text-transform: uppercase;
}

/* Usually a single block, but a project can flag more than one leading
   block as hero (see block.hero in the data) to keep several full-width
   items - e.g. two videos - together above Context instead of splitting
   them across the context panel. */
.project-hero {
  display: flex;
  flex-direction: column;
  gap: 60px;
  margin-bottom: 60px;
}

/* Two-column info panel below the hero media: left is the fixed
   client/role(s)/date facts every project has, right is free for a
   longer write-up when a project has one (empty otherwise). */
.project-context {
  padding-bottom: 30px;
}

.project-context-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 60px;
}

/* No description column for this project: give the info column (role/
   client/date) the full width instead of leaving it squeezed into half
   the row next to an empty second column. */
.project-context-grid.no-description {
  grid-template-columns: 1fr;
}

.project-context-title {
  color: var(--accent);
  font-size: 1.5rem;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.02em;
}

.project-field {
  margin-top: 12px;
}

/* Both inline (the value is a <div> by default) so label and value share
   one text flow - a long value (several roles, say) wraps like an
   ordinary sentence instead of the whole thing jumping to its own line,
   which is what happened when this was a flex row instead. */
.project-field-label,
.project-field-value {
  display: inline;
}

.project-field-label {
  color: var(--text);
  font-weight: 300;
  font-size: 1.15rem;
  letter-spacing: -0.01em;
}

.project-field-value {
  color: var(--text-muted);
  font-weight: 300;
  font-size: 1.15rem;
  letter-spacing: -0.01em;
}

.project-field-value .roles-sep {
  color: var(--accent);
}

/* The one piece of this panel meant to hold real prose (when a project has
   a longer write-up) - sized up from the rest of the panel's fact-list
   text instead of matching it. */
.project-context-description {
  color: var(--text-muted);
  font-size: 1.3rem;
  letter-spacing: -0.01em;
  line-height: 1.7;
}

.project-context-description p + p {
  margin-top: 16px;
}

.blocks {
  display: flex;
  flex-direction: column;
  gap: 60px;
  padding: 0 var(--edge) 120px;
  max-width: var(--max-w);
  margin: 0 auto;
}

.block-image img {
  width: 100%;
  border: 1px solid var(--border);
}

.block-caption {
  color: var(--text-muted);
  font-size: 0.85rem;
  margin-top: 10px;
}

.block-text {
  max-width: 760px;
  font-size: 1.05rem;
  color: var(--text-muted);
}

.block-text p + p {
  margin-top: 16px;
}

.block-gallery {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 16px;
}

.block-gallery img,
.block-thumb-magnify img {
  cursor: zoom-in;
  border: 1px solid var(--border);
}

/* Mixed-size mosaic layout — an optional look for the gallery block
   (block.layout === "bento"), used when a couple of shots deserve more
   room than the uniform 3-col grid gives them. */
.block-gallery-bento {
  grid-template-columns: repeat(2, 1fr);
  grid-auto-rows: 360px;
}

.block-gallery-bento img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.block-gallery-bento .gallery-span-tall {
  grid-row: span 2;
}

.block-thumb-magnify {
  max-width: 360px;
}

.block-quote {
  border-left: 3px solid var(--accent);
  padding-left: 24px;
  font-size: 1.3rem;
  font-weight: 500;
  max-width: 760px;
}

.block-quote cite {
  display: block;
  margin-top: 12px;
  font-style: normal;
  color: var(--text-muted);
  font-size: 0.85rem;
}

/* Muted side-note box - a technical detail worth isolating from the main
   flow (see renderCallout in js/blocks.js), deliberately quieter than
   .block-quote above (no large pull-quote type). */
.block-callout {
  padding: 18px 22px;
  background: var(--bg-alt);
  border-left: 3px solid var(--accent);
}

.block-callout-label {
  display: block;
  color: var(--accent);
  font-size: 0.75rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  margin-bottom: 8px;
}

.block-callout-body {
  color: var(--text-muted);
  font-size: 0.95rem;
  line-height: 1.6;
}

.block-callout-body p + p {
  margin-top: 10px;
}

.block-credits .project-context-title,
.block-credits-list .project-context-title {
  margin-bottom: 12px;
}

.block-credits table {
  border-collapse: collapse;
  width: 100%;
  max-width: 640px;
}

.block-credits td {
  padding: 10px 0;
  border-top: 1px solid var(--border);
  color: var(--text-muted);
  font-size: 1.15rem;
  letter-spacing: -0.01em;
}

.block-credits td:first-child {
  color: var(--text);
}

.block-credits-list {
  display: flex;
  flex-direction: column;
  gap: 6px;
  max-width: 720px;
}

.block-credits-list .credits-row {
  font-size: 1.15rem;
  letter-spacing: -0.01em;
  color: var(--text-muted);
  line-height: 1.6;
}

.block-credits-list .credits-row-break {
  margin-top: 18px;
}

.block-credits-list .credits-role {
  color: var(--text);
  font-weight: 700;
}

.block-credits-list .credits-sep {
  margin-right: 6px;
}

.block-credits-list .credits-names {
  color: var(--text-muted);
}

.block-video .video-wrap {
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
  background: var(--bg-alt);
  border: 1px solid var(--border);
}

.block-video iframe,
.block-video video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
  object-fit: cover;
}

/* Self-hosted clip loading state (see renderVideoFile, js/blocks.js) - a
   video-camera icon plus spinner fills the gap while the clip buffers, so
   it reads as "a video is loading" rather than a blank/broken box. Fades
   out once the video reports its first decoded frame (is-loaded). */
.video-loading {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 14px;
  background: var(--bg-alt);
  color: var(--text-muted);
  opacity: 1;
  transition: opacity 0.3s ease;
  pointer-events: none;
}

.video-wrap.is-loaded .video-loading {
  opacity: 0;
}

.video-loading-icon {
  width: 30px;
  height: 30px;
}

.video-loading-spinner {
  width: 20px;
  height: 20px;
  border: 2px solid var(--border);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: video-loading-spin 0.8s linear infinite;
}

@keyframes video-loading-spin {
  to {
    transform: rotate(360deg);
  }
}

/* Secondary videos (breakdowns, tests...) - smaller grid of thumbnails next
   to the main video-embed, each opening the same popup a video project card
   uses (see .card-video below for the matching play-button treatment). */
.block-video-gallery {
  display: grid;
  /* Fixed max column width (not 1fr) so a single item stays a comfortable,
     clickable size instead of stretching to fill the whole row. */
  grid-template-columns: repeat(auto-fill, minmax(320px, 420px));
  gap: 20px;
}

/* Two or more videos: same fixed 2-up layout as the image gallery's bento
   mode (.block-gallery-bento) below it, so both fill the full row width. */
.block-video-gallery:has(> :nth-child(2)) {
  grid-template-columns: repeat(2, 1fr);
}

.video-gallery-item {
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
}

.video-gallery-thumb {
  position: relative;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  background: var(--bg-alt);
  border: 1px solid var(--border);
}

.video-gallery-thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 0.4s cubic-bezier(0.22, 1, 0.36, 1);
}

.video-gallery-item:hover .video-gallery-thumb img {
  transform: scale(1.03);
}

.video-gallery-thumb::after {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: 44px;
  height: 44px;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.55);
  border: 1px solid rgba(255, 255, 255, 0.4);
  transition: transform 0.3s ease, background 0.3s ease, border-color 0.3s ease;
}

.video-gallery-thumb::before {
  content: '';
  position: absolute;
  top: 50%;
  left: calc(50% + 2px);
  width: 0;
  height: 0;
  border-style: solid;
  border-width: 7px 0 7px 12px;
  /* Fixed white, not var(--text) - the circle behind it is always a dark,
     semi-transparent scrim regardless of theme, so this needs to stay
     light to read against it in both dark and light mode. */
  border-color: transparent transparent transparent #f2f2f2;
  transform: translate(-50%, -50%);
  z-index: 1;
}

.video-gallery-item:hover .video-gallery-thumb::after {
  background: var(--accent);
  border-color: var(--accent);
  transform: translate(-50%, -50%) scale(1.08);
}

.video-gallery-caption {
  margin-top: 10px;
  font-size: 0.9rem;
  color: var(--text-muted);
}

.ba-slider {
  position: relative;
  aspect-ratio: 16 / 10;
  overflow: hidden;
  border: 1px solid var(--border);
  user-select: none;
}

.ba-slider img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.ba-slider .ba-after {
  clip-path: inset(0 0 0 50%);
}

.ba-handle {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 50%;
  width: 2px;
  background: var(--accent);
  transform: translateX(-1px);
  pointer-events: none;
}

.ba-handle::after {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: 34px;
  height: 34px;
  border-radius: 50%;
  background: var(--accent);
  transform: translate(-50%, -50%);
}

.ba-range {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  cursor: ew-resize;
  margin: 0;
}

/* Before/After pinned to the ends; the optional caption (see block.caption
   in renderBeforeAfter, js/blocks.js) sits centered between them on the
   same line - an empty middle span still keeps Before/After pushed apart
   exactly like justify-content: space-between did before captions existed. */
.ba-labels {
  display: flex;
  align-items: baseline;
  color: var(--text-muted);
  font-size: 0.8rem;
  margin-top: 10px;
}

.ba-label-before,
.ba-label-after {
  flex: 0 0 auto;
}

.ba-caption {
  flex: 1;
  text-align: center;
  padding: 0 16px;
  font-style: italic;
}

/* ---------- Lightbox ---------- */
.lightbox {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.92);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 40px;
  z-index: 200;
}

.lightbox[hidden] {
  display: none;
}

.lightbox img {
  max-width: 100%;
  max-height: 100%;
}

.lightbox-close {
  position: absolute;
  top: 24px;
  right: 32px;
  background: none;
  border: none;
  /* Fixed, not var(--text) - the backdrop behind it stays dark in both
     themes (see .lightbox above), so this can't follow the site theme. */
  color: #f2f2f2;
  font-size: 2rem;
  cursor: pointer;
  line-height: 1;
}

/* ---------- Video Lightbox ---------- */
.video-lightbox {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.92);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 40px;
  z-index: 200;
}

.video-lightbox[hidden] {
  display: none;
}

.video-lightbox-panel {
  position: relative;
  width: 100%;
  max-width: 1500px;
}

.video-lightbox-header {
  margin-bottom: 10px;
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
}

/* This whole popup (title/meta/roles/credits below) is fixed "dark chrome"
   text - #f2f2f2 / #9a9a9a rather than var(--text)/var(--text-muted) -
   because it always sits over the same dark backdrop (see .video-lightbox
   above) regardless of the site's light/dark theme. Using the theme
   variables here made the text unreadable in light mode. */
.video-lightbox-title {
  color: #f2f2f2;
  font-weight: 700;
  font-size: 1.1rem;
  text-transform: uppercase;
  letter-spacing: -0.02em;
}

/* Same size as the credits dropdown (.video-lightbox-credits below) - easy
   place to retune both: this pair for client/date + roles, that one for
   the credits list. */
.video-lightbox-meta {
  margin-top: 4px;
  color: #9a9a9a;
  font-size: 1rem;
}

/* Roles come straight from the data (already properly capitalized) and are
   joined into a single string with separators, so a lowercase + ::first-letter
   trick here would only ever fix the first role in the list. */
.video-lightbox-roles {
  margin-top: 3px;
  color: #9a9a9a;
  font-size: 1rem;
  font-weight: 600;
  letter-spacing: 0.01em;
}

.video-lightbox-roles .meta-sep,
.video-lightbox-meta .meta-sep {
  color: var(--accent);
}

.video-lightbox .video-wrap {
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
  background: #292929;
  border: 1px solid #3a3a3a;
}

.video-lightbox iframe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
}

.video-lightbox-close {
  position: absolute;
  top: 24px;
  right: 32px;
  background: none;
  border: none;
  color: #f2f2f2;
  font-size: 2rem;
  cursor: pointer;
  line-height: 1;
}

/* Credits toggle + dropdown - lets a video-only project (no full page)
   still surface its crew credits, without pushing the video around: the
   panel floats over it instead. */
.video-lightbox-credits-toggle {
  flex-shrink: 0;
  background: none;
  border: 1px solid #3a3a3a;
  color: #f2f2f2;
  font-family: inherit;
  font-size: 0.9rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  padding: 8px 14px;
  cursor: pointer;
  transition: border-color 0.2s ease, color 0.2s ease;
}

.video-lightbox-credits-toggle:hover,
.video-lightbox-credits-toggle[aria-expanded="true"] {
  border-color: var(--accent);
  color: var(--accent);
}

.video-lightbox-credits {
  position: absolute;
  top: 0;
  right: 0;
  z-index: 1;
  width: min(360px, 100%);
  max-height: calc(100% - 40px);
  overflow-y: auto;
  background: #141414;
  border: 1px solid #3a3a3a;
  padding: 20px 24px;
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.5);
}

.video-lightbox-credits[hidden] {
  display: none;
}

.video-lightbox-credits .block-credits-list,
.video-lightbox-credits .block-credits {
  max-width: none;
}

/* Fixed dark-chrome text again (see the comment above .video-lightbox-title)
   - .project-context-title/.credits-role/.credits-names/td otherwise pull
   var(--text)/var(--text-muted)/var(--accent-adjacent) from the site theme. */
.video-lightbox-credits .project-context-title {
  color: var(--accent);
  font-size: 1rem;
}

.video-lightbox-credits .credits-role,
.video-lightbox-credits td:first-child {
  color: #f2f2f2;
}

.video-lightbox-credits .credits-names,
.video-lightbox-credits td {
  color: #9a9a9a;
}

/* Smaller than the default credits block (see .block-credits-list below) -
   this dropdown floats over the video in a fairly narrow popup, so it stays
   compact rather than matching the full project page's larger size. */
.video-lightbox-credits .credits-row,
.video-lightbox-credits td {
  font-size: 0.9rem;
}

/* ---------- Blog ---------- */
.notes-list {
  display: flex;
  flex-direction: column;
}

.note-item {
  display: block;
  padding: 18px 0;
  text-decoration: none;
  color: inherit;
}

.note-title {
  font-weight: 600;
  font-size: 1.4rem;
  letter-spacing: -0.02em;
  text-transform: uppercase;
}

.note-meta {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: 4px;
  font-size: 0.95rem;
  color: var(--text-muted);
}

.note-meta-sep {
  color: var(--text-muted);
  opacity: 0.55;
}

.note-category {
  color: var(--accent);
  font-size: 0.95rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.note-body {
  display: grid;
  grid-template-columns: 220px 1fr;
  gap: 28px;
  margin-top: 22px;
}

.note-thumb {
  aspect-ratio: 16 / 10;
  overflow: hidden;
  background: var(--bg-alt);
  border: 1px solid var(--border);
}

.note-thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  filter: saturate(0.8) contrast(0.94) brightness(0.95);
  transition: transform 0.7s cubic-bezier(0.22, 1, 0.36, 1),
              filter 0.7s cubic-bezier(0.22, 1, 0.36, 1);
}

.note-item:hover .note-thumb img {
  transform: scale(1.03);
  filter: saturate(1) contrast(1) brightness(1);
}

.note-text {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  min-height: 100%;
}

.note-excerpt {
  color: var(--text-muted);
  font-size: 1.1rem;
  line-height: 1.65;
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.note-readmore {
  align-self: flex-end;
  margin-top: 16px;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 0.85rem;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--text-muted);
  transition: color 0.2s ease;
}

.note-readmore svg {
  width: 14px;
  height: 14px;
  transition: transform 0.2s ease;
}

.note-item:hover .note-readmore {
  color: var(--accent);
}

.note-item:hover .note-readmore svg {
  transform: translateX(4px);
}

/* ---------- Contact page ---------- */
.contact-layout {
  padding-top: 70px;
  padding-bottom: 120px;
  display: flex;
  justify-content: flex-start;
  align-items: stretch;
  gap: 32px;
}

.contact-main {
  max-width: 480px;
}

.contact-intro {
  color: var(--text-muted);
  font-size: 1.15rem;
  font-weight: 300;
  letter-spacing: -0.01em;
  margin-top: 20px;
}

.contact-email {
  display: inline-block;
  margin-bottom: 20px;
  font-size: 1.1rem;
  font-weight: 500;
  color: var(--text);
  text-decoration: none;
  letter-spacing: -0.01em;
  border-bottom: 1px solid var(--border);
  transition: color 0.2s ease, border-color 0.2s ease;
}

.contact-email:hover {
  color: var(--accent);
  border-color: var(--accent);
}

/* Reuses the project-page fact-list look (.project-field etc.) - same
   role:value rhythm as a project's Context panel, applied to freelance
   practicalities instead. */
.contact-facts {
  margin-top: 8px;
  max-width: 480px;
}

.contact-columns {
  display: flex;
  flex-wrap: wrap;
  gap: 80px;
  margin-top: 56px;
}

.contact-label {
  display: flex;
  align-items: center;
  gap: 10px;
  color: var(--text);
  font-size: clamp(1.8rem, 3.4vw, 2.4rem);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: -0.03em;
  margin-bottom: 24px;
}

.contact-label-arrow {
  flex-shrink: 0;
  width: 1.5em;
  height: 1.5em;
  color: var(--accent);
}

.contact-social-list {
  display: flex;
  flex-wrap: wrap;
  gap: 20px;
}

.contact-social-list a {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 56px;
  height: 56px;
  border-radius: 50%;
  border: 1px solid var(--border);
  color: var(--text);
  transition: color 0.2s ease, border-color 0.2s ease;
}

.contact-social-list a svg {
  width: 26px;
  height: 26px;
}

.contact-social-list a:hover {
  color: var(--accent);
  border-color: var(--accent);
}

/* No fixed aspect-ratio: align-items: stretch on .contact-layout makes this
   match .contact-main's full height (title down to the social icons). */
.contact-portrait {
  flex: 0 0 380px;
  width: 380px;
  overflow: hidden;
  background: var(--bg-alt);
}

.contact-portrait img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

@media (max-width: 900px) {
  .contact-layout {
    flex-direction: column;
    gap: 40px;
  }

  .contact-portrait {
    flex: 0 0 auto;
    width: 100%;
    max-width: 420px;
    aspect-ratio: 16 / 9;
  }
}

/* ---------- Note page ---------- */
/* Horizontal inset comes from the nested .wrap (see note.html), same as
   .project-header on the project page - padding it here too would double
   it up against .note-content below, which applies --edge just once. */
.note-header {
  padding: 140px 0 40px;
}

.note-content {
  padding: 0 var(--edge) 120px;
  color: var(--text-muted);
  font-size: 1.15rem;
}

/* clear: both - without it, a heading that falls within the vertical range
   of a still-active earlier float (see .block-float-right/-left) wraps
   into the narrowed column beside it instead of starting a new, full-width
   section. Headings should always read as a clean section break. */
/* Same weight/tracking as .note-title (the blog thumbnail card) so an
   article's section titles read as the same "object" as its card - only
   the weight/tracking carry over, this keeps its own font-size. */
.note-content h1,
.note-content h2,
.note-content h3 {
  clear: both;
  color: var(--text);
  font-weight: 600;
  letter-spacing: -0.02em;
  font-size: clamp(1.7rem, 3vw, 2.3rem);
  margin: 40px 0 16px;
}

.note-content h1:first-child,
.note-content h2:first-child {
  margin-top: 0;
}

/* A heading immediately AFTER a floated block (see .block-float-right/-left
   below) wraps beside it instead of clearing it - a float only pushes
   around content that follows it, so this only works this direction (the
   block has to come first in the markdown). This is what puts a note's
   opening title in the left column, above its intro paragraph, alongside
   the block instead of dropping clear below it. */
.note-content .block-float-right + h1,
.note-content .block-float-right + h2,
.note-content .block-float-left + h1,
.note-content .block-float-left + h2 {
  clear: none;
  margin-top: 4px;
}

.note-content p,
.note-content ul,
.note-content ol,
.note-content blockquote,
.note-content pre {
  margin-bottom: 18px;
}

.note-content p {
  text-align: justify;
}

.note-content ul,
.note-content ol {
  padding-left: 22px;
}

.note-content a {
  color: var(--accent);
}

.note-content pre {
  background: var(--bg-alt);
  border: 1px solid var(--border);
  padding: 18px;
  overflow-x: auto;
  font-size: 0.92rem;
}

.note-content blockquote {
  border-left: 3px solid var(--accent);
  padding-left: 20px;
  color: var(--text);
}

/* Spacing for embedded ```block fences (see page-note.js) - the block
   renderers themselves carry no margin, they normally rely on .blocks'
   flex gap for spacing between siblings. */
.note-content .block-image,
.note-content .block-gallery,
.note-content .block-video,
.note-content .block-before-after,
.note-content .block-thumb-magnify,
.note-content .block-callout {
  margin: 8px 0 28px;
  clear: both;
}

/* Opt-in per block (see renderBlock in js/blocks.js) - floats a block so
   the paragraph right after it (typically a note's intro text) wraps
   beside it instead of stacking full-width underneath. clear: none
   overrides the default clear: both above, which would otherwise cancel
   the float on the element carrying it. */
.note-content .block-float-right,
.note-content .block-float-left {
  clear: none;
  max-width: 380px;
  margin-top: 4px;
  margin-bottom: 24px;
}

.note-content .block-float-right {
  float: right;
  margin-left: 36px;
}

.note-content .block-float-left {
  float: left;
  margin-right: 36px;
}

/* Opt-in per block (see renderBlock in js/blocks.js) - lets a non-floated
   block sit beside a still-active earlier float instead of waiting for it
   to clear (the default). Only works visually alongside a narrow `width`
   override - a block this wide relative to its container just won't reach
   into the float's reserved space, so no positioning trick is needed. */
.note-content .block-no-clear {
  clear: none;
}

/* The italic caption line markdown authors put right after a gallery fence
   (it has no native caption slot, unlike image-full's `caption` field, or
   before-after's - see .ba-caption) - pull it up against the block instead
   of reading as its own paragraph. */
.note-content .block-gallery + p em {
  color: var(--text-muted);
  font-size: 0.95rem;
}

.note-content .block-gallery + p {
  margin-top: -18px;
}

/* Scoped, larger-than-project-page sizing for the shared block renderers
   (see js/blocks.js) - notes read as long-form articles, so their embedded
   media captions/labels get a bit more size than a project page's terser
   ones. */
.note-content .block-caption {
  font-size: 0.95rem;
}

.note-content .ba-labels {
  font-size: 0.9rem;
}

.note-content .block-callout-label {
  font-size: 0.95rem;
}

.note-content .block-callout-body {
  font-size: 1.05rem;
}

/* ---------- Email copy toast ---------- */
/* Fixed + JS-positioned (see js/copy-email.js) right above whichever email
   link/icon was clicked, anywhere on the site. Always dark chrome on the
   accent colour, like the video lightbox text - simplest way to stay
   legible regardless of the site's light/dark theme. */
.email-copied-toast {
  position: fixed;
  transform: translate(-50%, calc(-100% - 10px));
  background: var(--accent);
  color: #171717;
  font-size: 0.8rem;
  font-weight: 600;
  letter-spacing: -0.01em;
  padding: 6px 12px;
  white-space: nowrap;
  pointer-events: none;
  z-index: 300;
  opacity: 0;
  transition: opacity 0.2s ease, transform 0.2s ease;
}

.email-copied-toast.is-visible {
  opacity: 1;
  transform: translate(-50%, calc(-100% - 16px));
}

/* ---------- Footer ---------- */
.site-footer {
  min-height: var(--header-h);
  background-color: var(--bg-header-solid);
  color: var(--text-muted);
  font-size: 0.8rem;
  display: flex;
  align-items: center;
}

/* Same edge-alignment trick as .header-inner above. */
.footer-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 12px;
  width: 100%;
}

.footer-socials {
  display: flex;
  align-items: center;
  gap: 16px;
}

.footer-socials a {
  display: block;
  width: 20px;
  height: 20px;
  color: var(--text-muted);
  transition: color 0.2s ease;
}

.footer-socials a:hover {
  color: var(--accent);
}

.footer-socials svg {
  width: 100%;
  height: 100%;
}

/* ---------- Responsive ---------- */
@media (max-width: 900px) {
  .grid,
  .category-grid {
    grid-template-columns: repeat(2, 1fr);
  }

  .block-gallery {
    grid-template-columns: repeat(2, 1fr);
  }
}

@media (max-width: 768px) {
  .nav-toggle {
    display: flex;
  }

  .main-nav {
    position: fixed;
    top: var(--header-h);
    left: 0;
    right: 0;
    bottom: 0;
    background: var(--bg-header-solid);
    flex-direction: column;
    gap: 0;
    padding: 20px 24px;
    transform: translateX(100%);
    transition: transform 0.3s ease;
  }

  .main-nav.is-open {
    transform: translateX(0);
  }

  .main-nav a {
    padding: 16px 0;
    border-bottom: 1px solid var(--border);
    font-size: 1.1rem;
  }

  .hero-pin,
  .wrap {
    padding-left: 24px;
    padding-right: 24px;
  }

  .grid,
  .category-grid,
  .block-gallery {
    grid-template-columns: 1fr;
  }

  .block-video-gallery:has(> :nth-child(2)) {
    grid-template-columns: 1fr;
  }

  .block-gallery-bento {
    grid-auto-rows: 220px;
  }

  .block-gallery-bento .gallery-span-tall {
    grid-row: auto;
  }

  .note-body {
    grid-template-columns: 1fr;
    gap: 16px;
  }

  .note-readmore {
    align-self: flex-start;
  }

  .blocks {
    padding-left: 24px;
    padding-right: 24px;
  }

  .stash-tagline {
    white-space: normal;
  }

  .about-layout {
    grid-template-columns: 1fr;
    gap: 32px;
  }

  .about-portrait {
    max-width: 220px;
    justify-self: start;
  }
}
