:root {
  --ink: #15151A;
  --paper: #F3EEE4;
  --indigo: #2B3A67;
  --ochre: #D98E2B;
  --rule: #C9C2B2;
  --red: #ba3c3c;
  --display: "Outfit", sans-serif;
  --body: "Inter", sans-serif;
}

* { box-sizing: border-box; }

html { scroll-behavior: smooth; overflow-x: hidden; }

/* Custom cursor — a small hollow ring that follows the mouse and
   fills in solid when hovering an interactive element (see the JS
   in index.html for the hover-detection logic). Base rule (with
   display:none) comes BEFORE the media query below — two rules
   targeting the same #custom-cursor selector have equal specificity,
   so whichever comes later in the file wins regardless of the media
   query matching. Base-rule-first lets the media query's
   display:block correctly override it when it matches. */
#custom-cursor {
  display: none;
  position: fixed;
  top: 0;
  left: 0;
  width: 15px;
  height: 15px;
  border: 2px solid var(--red);
  border-radius: 50%;
  background: transparent;
  transform: translate(-50%, -50%);
  pointer-events: none;
  z-index: 9999;
  transition: background-color 0.15s ease, width 0.15s ease, height 0.15s ease;
}
#custom-cursor::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  border: 1px solid var(--red);
  opacity: 0;
  pointer-events: none;
}
#custom-cursor.cursor-hover::after {
  animation: cursor-pulse-ring 1s ease-out infinite;
}
#custom-cursor.cursor-hover {
  background: var(--red);
  width: 20px;
  height: 20px;
}
@media (hover: hover) and (pointer: fine) {
  body {
    cursor: none;
  }
  * {
    cursor: none !important;
  }
  #custom-cursor {
    display: block;
  }
  /* Form fields keep their native text caret — the custom ring can't
     show exact cursor position within typed text the way a real
     caret does. */
  input, textarea {
    cursor: auto !important;
  }
}
@media (prefers-reduced-motion: reduce) {
  #custom-cursor.cursor-hover::after {
    animation: none;
  }
}

/* Header background bar — dark fill behind the logo/nav, which now
   stay in their light styling everywhere except over an open popup
   (see the JS). Fades in once scrolled past the hero. */
.header-bar-bg {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 90px;
  z-index: 240;
  background: var(--ink);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.3s ease;
}
.header-bar-bg.is-visible {
  opacity: 1;
}
@media (max-width: 600px) {
  .header-bar-bg { height: 70px; }
}

/* Scroll-to-top — dark circle, paper-colored arrow, fixed
   bottom-right. Hidden until scrolled past the hero, and hidden again
   whenever any popup is open (handled in JS via the [hidden] attribute). */
.scroll-top-btn {
  position: fixed;
  bottom: 1.5rem;
  right: 1.5rem;
  z-index: 250;
  width: 48px;
  height: 48px;
  border-radius: 50%;
  border: none;
  background: var(--ink);
  color: var(--paper);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.3s ease, transform 0.15s ease;
}
.scroll-top-btn.is-visible {
  opacity: 1;
  pointer-events: auto;
}
.scroll-top-btn svg {
  width: 20px;
  height: 20px;
}
.scroll-top-btn:hover {
  transform: translateY(-2px);
}
.scroll-top-btn:focus-visible {
  outline: 2px solid var(--ochre);
  outline-offset: 2px;
}
@media (max-width: 600px) {
  .scroll-top-btn { bottom: 1rem; right: 1rem; width: 42px; height: 42px; }
  .scroll-top-btn svg { width: 17px; height: 17px; }
}
@media (prefers-reduced-motion: reduce) {
  .scroll-top-btn { transition: none; }
}

/* Site logo — fixed top-left, mirrors social icons */
.site-logo {
  position: fixed;
  top: 1.25rem;
  left: 1.25rem;
  z-index: 250;
  height: 40px;
  display: flex;
  align-items: center;
}
.site-logo a {
  display: flex;
  align-items: center;
}
.site-logo img {
  height: 34px;
  width: auto;
  display: block;
}
.site-logo a:focus-visible {
  outline: 2px solid var(--ochre);
  outline-offset: 2px;
}
@media (max-width: 600px) {
  .site-logo { top: 0.85rem; left: 0.85rem; }
  .site-logo img { height: 26px; }
}

/* Header nav — fixed top-right, persistent across scroll. Combines
   the Work/About/Contact links with the social icons as one group so
   they don't read as two different UI languages sitting side by side.
   No background chip: color alone carries contrast, switched
   light/dark based on scroll position past the hero (see JS). */
.header-nav {
  position: fixed;
  top: 1.25rem;
  right: 1.25rem;
  z-index: 250;
  height: 40px;
  display: flex;
  align-items: center;
  gap: 1.1rem;
}
.nav-link {
  font-size: 0.85rem;
  font-weight: 500;
  color: var(--paper);
  background: transparent;
  padding: 0.35rem 0.75rem;
  text-decoration: none;
  transition: color 0.2s ease, background-color 0.2s ease;
}
.nav-link:hover {
  background: var(--ink);
}
.nav-link.is-active {
  background: var(--ochre);
}
.nav-link:focus-visible {
  outline: 2px solid var(--ochre);
  outline-offset: 2px;
}
.header-nav.is-past-hero .nav-link {
  color: var(--ink);
}
.header-nav.is-past-hero .nav-link:hover {
  background: #FFFFFF;
}
.header-nav.is-past-hero .nav-link.is-active {
  background: var(--ochre);
}
.nav-divider {
  width: 1px;
  height: 16px;
  background: currentColor;
  color: var(--paper);
  opacity: 0.35;
  transition: color 0.2s ease;
}
.header-nav.is-past-hero .nav-divider {
  color: var(--ink);
}
.social-icons {
  display: flex;
  gap: 0.6rem;
}
.about-social-icons {
  margin: 0.75rem 0;
}
.about-social-icons .social-icon {
  width: 52px;
  height: 52px;
}
.about-social-icons .social-icon svg {
  width: 24px;
  height: 24px;
}
@keyframes cursor-pulse-ring {
  0% {
    transform: scale(1);
    opacity: 0.9;
  }
  100% {
    transform: scale(3.5);
    opacity: 0;
  }
}
.social-icon {
  width: 40px;
  height: 40px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--paper);
  transition: color 0.2s ease, opacity 0.2s ease;
}
.header-nav.is-past-hero .social-icon {
  color: var(--ink);
}
.social-icon svg {
  width: 18px;
  height: 18px;
}
.social-icon:hover {
  opacity: 0.65;
}
.social-icon:focus-visible {
  outline: 2px solid var(--ochre);
  outline-offset: 2px;
}
@media (max-width: 600px) {
  .header-nav { top: 0.85rem; right: 0.85rem; gap: 0.7rem; }
  .nav-link { font-size: 0.78rem; }
  .social-icons { gap: 0.4rem; }
  .social-icon { width: 34px; height: 34px; }
  .social-icon svg { width: 15px; height: 15px; }
}

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--body);
  font-size: 0.9375rem;
  line-height: 1.5;
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
}

a { color: inherit; }

/* Masthead */
.masthead {
  position: sticky;
  top: 0;
  z-index: 1;
  min-height: 100svh;
  display: flex;
  align-items: flex-end;
  overflow: hidden;
  border-bottom: 1px solid var(--rule);
  background: var(--ink); /* shows before video paints / if video fails */
}
.masthead-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  z-index: 0;
}
.masthead-scrim {
  position: absolute;
  inset: 0;
  background: linear-gradient(0deg, rgba(21,21,26,0.82) 0%, rgba(21,21,26,0.35) 55%, rgba(21,21,26,0.15) 100%);
  z-index: 1;
}
.masthead-inner {
  position: relative;
  z-index: 2;
  max-width: 780px;
  padding: 3rem 1.5rem 4rem;
  color: var(--paper);
}
.eyebrow-plain {
  font-size: 0.8rem;
  color: var(--ochre);
  margin: 0 0 0.5rem;
}
.masthead h1 {
  font-family: var(--display);
  font-optical-sizing: auto;
  font-weight: 900;
  font-size: clamp(3.2rem, 9vw, 6rem);
  line-height: 0.98;
  margin: 0 0 1.2rem;
  letter-spacing: -0.01em;
  color: var(--paper);
}
.role {
  font-size: 1.05rem;
  max-width: 52ch;
  margin: 0 0 2rem;
  color: var(--paper);
  line-height: 1.55;
}
.role-link {
  color: var(--ochre);
  text-decoration: underline;
  text-underline-offset: 3px;
}
.cta {
  display: inline-block;
  padding: 0.75rem 1.4rem;
  text-decoration: none;
  font-weight: 500;
  border: 1px solid var(--paper);
  transition: transform 0.15s ease;
}
.cta:hover {
  animation: cta-shake 0.3s ease-in-out infinite;
}
@keyframes cta-shake {
  0%, 100% { transform: scale(1.05) rotate(0deg); }
  25% { transform: scale(1.05) rotate(-1deg); }
  75% { transform: scale(1.05) rotate(1deg); }
}
@media (prefers-reduced-motion: reduce) {
  .cta:hover {
    animation: none;
    transform: scale(1.05);
  }
}
.cta-primary { background: var(--paper); color: var(--ink); }
.cta-primary:hover { background: var(--ochre); border-color: var(--ochre); color: var(--ink); }
.cta-secondary { background: transparent; color: var(--paper); }
.cta-secondary:hover { background: var(--paper); color: var(--ink); }
.project-view-link {
  display: inline-block;
  margin: 0.25rem 0 1rem;
}
.project-view-link[hidden] {
  display: none;
}

/* Respect reduced motion: freeze on poster frame instead of autoplaying */
@media (prefers-reduced-motion: reduce) {
  .masthead-video { display: none; }
  .masthead {
    background-image: var(--poster-fallback, none);
    background-size: cover;
    background-position: center;
  }
}

/* Client logo marquee */
/* Solid, unmasked backdrop — sits behind the actual masked marquee
   below. mask-image fades an element's entire rendering, background
   included, so without this separate solid layer, the faded edges
   would let whatever's behind in the parallax stack (the sticky hero
   video) show through instead of fading to solid paper. */
.client-marquee-wrap {
  position: relative;
  z-index: 2;
  background: var(--paper);
}
.client-marquee {
  overflow: hidden;
  padding: 1.75rem 0;
  -webkit-mask-image: linear-gradient(to right, transparent 0%, black 30%, black 70%, transparent 100%);
  mask-image: linear-gradient(to right, transparent 0%, black 30%, black 70%, transparent 100%);
}
.marquee-track {
  display: flex;
  align-items: center;
  width: max-content;
  animation: marquee-scroll 40s linear infinite;
}
.marquee-logo {
  height: 40px;
  width: auto;
  margin: 0 1.5rem;
  flex-shrink: 0;
  filter: grayscale(100%) contrast(0.9) brightness(0.85);
  opacity: 0.6;
  pointer-events: none;
  user-select: none;
}
@keyframes marquee-scroll {
  from { transform: translateX(0); }
  to { transform: translateX(-50%); }
}

/* Reduced motion: static wrapped row, no animation, no duplicate set needed
   visually (the JS still renders it twice, so hide the second copy). */
@media (prefers-reduced-motion: reduce) {
  .marquee-track {
    animation: none;
    flex-wrap: wrap;
    width: auto;
    justify-content: center;
  }
  .marquee-track img.marquee-dup {
    display: none;
  }
}

/* Section shared */
main {
  position: relative;
  z-index: 2;
  background: var(--paper);
}
main section {
  padding: 4rem 1.5rem;
  max-width: 1100px;
  margin: 0 auto;
}
/* Selected Work + More Work only — wider than the standard content
   width so the grid (and its cards) get more room, without widening
   Thud/About/Contact which share this same base rule. */
#work {
  max-width: 1450px;
}
h2 {
  font-family: var(--display);
  font-weight: 800;
  font-size: clamp(2.1rem, 4.5vw, 3rem);
  margin: 0 0 0.35rem;
}
.title-accent {
  color: var(--red);
}

/* Work grid */
.work-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 1.25rem;
}
/* Settle-in entrance: each card starts offset by its own --enter-y
   (varied per item for a layered "different planes" feel), then
   settles into place once #work-grid scrolls ~15% into view (see JS).
   One-shot — triggers once and stays settled regardless of scroll
   direction afterward. Scoped to #work-grid specifically so it doesn't
   affect the More Work grid. */
#work-grid .work-item {
  opacity: 0;
  transform: translateY(var(--enter-y, 50px)) scale(0.97);
  transition: opacity 0.6s ease, transform 0.7s cubic-bezier(0.16, 1, 0.3, 1);
  transition-delay: var(--enter-delay, 0ms);
}
#work-grid.in-view .work-item {
  opacity: 1;
  transform: none;
}

@media (prefers-reduced-motion: reduce) {
  #work-grid .work-item {
    opacity: 1 !important;
    transform: none !important;
    animation: none !important;
    transition: none !important;
  }
}
.more-work-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 1.25rem;
  margin-top: 2rem;
  overflow: hidden;
  max-height: 0;
  transition: max-height 0.4s ease;
}
.more-work-grid:empty {
  margin-top: 0;
}
@media (prefers-reduced-motion: reduce) {
  .more-work-grid {
    transition: none;
  }
}
.work-item.span-2 { grid-column: span 2; }
.work-item.span-1 { grid-column: span 1; }

/* work-item is now a <button> — strip default button chrome */
.work-item {
  display: block;
  width: 100%;
  text-align: left;
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  color: inherit;
  cursor: pointer;
  position: relative;
  z-index: 0;
}
.work-item::before {
  content: '';
  position: absolute;
  inset: -0.75rem;
  background: #FFFFFF;
  z-index: -1;
  opacity: 0;
  transition: opacity 0.15s ease;
  pointer-events: none;
}
.work-item:hover::before {
  opacity: 1;
}
.work-item:focus-visible {
  outline: 2px solid var(--indigo);
  outline-offset: 4px;
}
/* The [hidden] attribute alone gets overridden by the display:block
   rule above (author CSS beats user-agent CSS regardless of
   specificity) — this compound selector wins back the actual hiding. */
.work-item[hidden] {
  display: none;
}

.more-work-row {
  margin-top: 2.5rem;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1rem;
}
.more-work-row .cta-secondary,
.cta.cta-dark {
  border-color: var(--ink);
  color: var(--ink);
}
.more-work-row .cta-secondary:hover,
.cta.cta-dark:hover {
  background: var(--ink);
  color: var(--paper);
}
.work-item-extra {
  opacity: 0;
  transform: translateY(28px);
  transition: opacity 0.45s ease, transform 0.45s ease;
}
.work-item-extra.is-visible {
  opacity: 1;
  transform: translateY(0);
}
@media (prefers-reduced-motion: reduce) {
  .work-item-extra {
    transition: none;
    opacity: 1;
    transform: none;
  }
}
/* Landscape rather than square — scoped to the "more work" extras
   only, using both classes for enough specificity to beat the plain
   .work-item.span-1 rule below. */
.work-item.span-1.work-item-extra .work-image {
  aspect-ratio: 4 / 3;
}
.work-image {
  aspect-ratio: 16 / 10;
  margin-bottom: 0.75rem;
  overflow: hidden;
}
/* span-1 is half the width of span-2 — this ratio is calculated (not
   guessed) to match span-2's actual height exactly at the content's
   1100px max-width, accounting for the 1.25rem grid gap. It'll drift
   by a percent or two at in-between viewport widths since the gap
   doesn't scale proportionally with the columns, but stays accurate
   at the width most people will actually view this at. */
.work-item.span-1 .work-image {
  aspect-ratio: 10 / 13;
}
.work-image.placeholder {
  background: repeating-linear-gradient(45deg, #E3DCCB, #E3DCCB 10px, #D8D0BB 10px, #D8D0BB 20px);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.85rem;
  color: var(--indigo);
}
.work-image img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform 0.3s ease;
}
.work-item:hover .work-image img {
  transform: scale(1.15);
}
.work-item h3 { margin: 0 0 0.3rem; font-size: 0.9375rem; }
.work-item p { margin: 0; font-size: 0.8rem; color: #4A4A4A; }

@media (max-width: 700px) {
  .work-grid { grid-template-columns: 1fr; }
  .more-work-grid { grid-template-columns: repeat(2, 1fr); }
  .work-item.span-2, .work-item.span-1 { grid-column: span 1; }
  .work-item.span-1 .work-image,
  .work-item.span-1.work-item-extra .work-image { aspect-ratio: 16 / 10; }
}

/* Project detail overlay — slides up from the bottom, takes over the page */
.project-overlay {
  position: fixed;
  inset: 0;
  z-index: 200;
  background: rgba(21, 21, 26, 0.5);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.25s ease;
}
.project-overlay.is-open {
  opacity: 1;
  pointer-events: auto;
}
.project-overlay-inner {
  /* position:relative (not absolute) is required here — Safari has a
     documented bug where position:sticky fails for any descendant
     when its scrolling ancestor (this element) is itself
     position:absolute with overflow:auto/scroll. Chrome and Firefox
     handle the absolute case fine; Safari doesn't. Sized with
     explicit width/height instead of inset, since relative
     positioning doesn't stretch to fill via top/left/right/bottom
     the way absolute does. */
  position: relative;
  width: 100%;
  height: 100%;
  top: 100%;
  background: var(--paper);
  transition: top 0.35s ease;
  overflow-y: auto;
}
.project-overlay.is-open .project-overlay-inner {
  top: 0;
}
@media (prefers-reduced-motion: reduce) {
  .project-overlay, .project-overlay-inner { transition: none; }
}
.project-close {
  position: sticky;
  top: 6.5rem;
  margin-left: calc(100% - 1.25rem - 44px);
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background: var(--paper);
  border: 1px solid var(--rule);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  color: var(--ink);
  z-index: 3;
}
.project-close svg { width: 18px; height: 18px; }
.project-close:focus-visible {
  outline: 2px solid var(--ochre);
  outline-offset: 2px;
}
.project-overlay-content {
  position: relative;
  z-index: 2;
  background: var(--paper);
  max-width: 900px;
  margin: 1.5rem auto 0;
  padding: 0 1.5rem;
}
/* Fun projects popup only — wider so 3 columns have room, without
   widening the per-project popup which shares this same base class. */
#fun-overlay .project-overlay-content {
  max-width: 1300px;
}
/* Collection popups (opened from a fun-project tile) match the same
   width as the fun-projects grid they're opened from. */
#collection-overlay .project-overlay-content {
  max-width: 1300px;
}
/* Project popup (opened from either Selected Work or More Work — both
   use this same overlay) matches the widened #work section on the
   main page. Also guarantees at least one viewport height of content,
   scoped to just this popup (not fun/collection, which don't have a
   sticky image and don't need the extra guaranteed height) — so the
   sticky main image always has enough room to fully release before
   the footer, regardless of how short a given project's own
   description/gallery happens to be. */
#project-overlay .project-overlay-content {
  max-width: 1450px;
  min-height: 100vh;
}
.project-client {
  margin-top: 0;
  font-size: 0.78rem;
  font-weight: 700;
  margin-bottom: 0.75rem;
  color: var(--ochre);
}
.project-client:empty {
  display: none;
}
.field-label {
  font-weight: 400;
}
.text-accent-red {
  color: var(--red);
  font-weight: 700;
}
#project-focus {
  margin-top: 0;
}
.project-overlay-content h2 {
  margin-top: .5rem;
  margin-bottom: 1.25rem;
}
/* Two-column layout: description on the left under the heading,
   Client/Work Done/Credits/View Project as a compact sidebar on the
   right. Stacks back to one column on narrower screens, description
   first, since that's the primary content. */
.project-top-grid {
  display: grid;
  grid-template-columns: 1.6fr 1fr;
  gap: 3rem;
  align-items: start;
  margin-bottom: 2rem;
}
@media (max-width: 800px) {
  .project-top-grid {
    grid-template-columns: 1fr;
    gap: 1rem;
  }
  .project-meta {
    /* Restores the original reading order (meta info before the long
       description) once stacked to one column on narrow screens —
       the two-column side-by-side layout is a desktop-width thing. */
    order: -1;
  }
}
.project-credits {
  font-size: 0.78rem;
  font-weight: 700;
  margin: 0 0 0.75rem;
  color: var(--ochre);
}
.project-credits:empty {
  display: none;
}
/* Approximates #project-overlay .project-overlay-content's box model
   (max-width minus its 3rem of horizontal padding) directly on the
   sticky element itself — not via a wrapper. A wrapper here would
   collapse position:sticky's containing block down to exactly the
   image's own height, leaving zero room to actually stick (this was
   a real, previously-shipped bug, not a hypothetical). This
   approximation is only exact once the viewport is wide enough for
   1450px to be the binding constraint; below that it can drift by a
   few pixels from the grid's own edges, which is an acceptable
   trade-off for a working sticky effect. */
.project-image {
  position: sticky;
  top: 0;
  z-index: 1;
  width: 100%;
  max-width: calc(1450px - 3rem);
  margin: 0 auto 1.5rem;
  display: block;
  overflow: hidden;
}
.project-image.placeholder {
  aspect-ratio: 21 / 9;
  background: repeating-linear-gradient(45deg, #E3DCCB, #E3DCCB 10px, #D8D0BB 10px, #D8D0BB 20px);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--indigo);
}
.project-image img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
#project-description {
  white-space: pre-line;
  margin-top: 0;
  max-width: 90ch;
}
#collection-description {
  white-space: pre-line;
}
#collection-description:empty {
  display: none;
}
.project-gallery {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 0.6rem;
  margin: 2rem 0;
  align-items: start;
}
/* Collection popups only (ABC Comics, etc.) — 4 columns, without
   touching the 2-column galleries in individual project popups, which
   share this same base class. */
#collection-grid {
  grid-template-columns: repeat(4, 1fr);
}
.gallery-tile {
  position: relative;
  width: 100%;
}
.gallery-tile-span-2 {
  grid-column: span 2;
}
.gallery-media {
  aspect-ratio: 4 / 3;
  overflow: hidden;
  background: #EDE7DA;
  position: relative;
}
.gallery-media img,
.gallery-media video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
/* Collection popups only (ABC Comics, etc.) — square, without
   touching the 4:3 galleries in individual project popups. */
#collection-grid .gallery-media {
  aspect-ratio: 1 / 1;
}
.gallery-tile.placeholder {
  aspect-ratio: 4 / 3;
  background: repeating-linear-gradient(45deg, #E3DCCB, #E3DCCB 10px, #D8D0BB 10px, #D8D0BB 20px);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.85rem;
  color: var(--indigo);
}
.gallery-tile img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
  transition: transform 0.3s ease;
}
.gallery-tile video {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
  transition: transform 0.3s ease;
}
.gallery-tile:hover img,
.gallery-tile:hover video {
  transform: scale(1.15);
}
.gallery-tile p {
  width: 100%;
  margin: 0.6rem 0 0;
  font-size: 0.8rem;
  color: #4A4A4A;
  text-align: center;
}
@media (max-width: 600px) {
  .project-gallery { grid-template-columns: 1fr; }
}
.project-overlay-footer {
  margin-top: 3rem;
}

/* Fun projects overlay — flat grid, no per-item detail */
.fun-projects-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 1.25rem;
  margin-top: 2.5rem;
  align-items: start;
}
.fun-category-header {
  grid-column: 1 / -1;
  font-family: var(--display);
  font-weight: 500;
  font-size: 1.3rem;
  color: var(--ink);
  margin: 2.5rem 0 0.25rem;
  padding-top: 1.5rem;
  border-top: 1px solid var(--rule);
  /* So a scrolled-to header doesn't land flush behind the sticky
     close button, which stays fixed near the top of the popup. */
  scroll-margin-top: 6.5rem;
}
.fun-category-header:first-child {
  margin-top: 0;
  padding-top: 0;
  border-top: none;
}
.fun-submenu {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  margin: 1.25rem 0 0;
}
.fun-project-image {
  aspect-ratio: 1 / 1;
  overflow: hidden;
  background: #EDE7DA;
  margin-bottom: 0.6rem;
  position: relative;
}
/* Instagram's embed widget sizes and scrolls itself — the fixed 4:3
   crop box that works for images/video would just clip it. */
.fun-project-image.fun-project-embed {
  aspect-ratio: auto;
  overflow: visible;
  background: none;
  display: flex;
  justify-content: center;
}
.fun-project-image.placeholder {
  background: repeating-linear-gradient(45deg, #E3DCCB, #E3DCCB 10px, #D8D0BB 10px, #D8D0BB 20px);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.85rem;
  color: var(--indigo);
}
.fun-project-image img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
  transition: transform 0.3s ease;
}
.fun-project-image video {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
  transition: transform 0.3s ease;
}
.fun-project-tile:hover .fun-project-image img,
.fun-project-tile:hover .fun-project-image video {
  transform: scale(1.15);
}
/* Play icon overlaid on video thumbnails — pointer-events: none so
   clicks pass through to the actual video element beneath it, which
   is what carries the .lightbox-trigger click handling. */
.play-icon {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
}
.play-icon svg {
  width: 44px;
  height: 44px;
  color: var(--paper);
  filter: drop-shadow(0 1px 4px rgba(0, 0, 0, 0.6));
}
.fun-project-tile {
  position: relative;
  z-index: 0;
}
.fun-project-tile::before {
  content: '';
  position: absolute;
  inset: -0.75rem;
  background: #FFFFFF;
  z-index: -1;
  opacity: 0;
  transition: opacity 0.15s ease;
  pointer-events: none;
}
.fun-project-tile:hover::before {
  opacity: 1;
}
.fun-project-tile p {
  margin: 0;
  font-size: 0.8rem;
  color: #4A4A4A;
}
@media (max-width: 700px) {
  .fun-projects-grid { grid-template-columns: 1fr; }
}
@media (min-width: 701px) and (max-width: 1000px) {
  .fun-projects-grid { grid-template-columns: repeat(2, 1fr); }
}

/* Body lock while overlay is open. html also needs to be explicitly
   locked here — once html has any explicit overflow-x value set (for
   the earlier horizontal-scrollbar fix), the browser stops letting
   body's own overflow:hidden propagate up to the page-level
   scrollbar, so without this, body locks correctly but html's own
   scrollbar keeps showing right alongside the popup's internal one. */
body.overlay-open {
  overflow: hidden;
}
html:has(body.overlay-open) {
  overflow-y: hidden;
}

/* Lightbox — click any popup image to view it full size */
.lightbox-trigger {
  cursor: zoom-in;
}
.collection-trigger {
  cursor: pointer;
}

.lightbox {
  position: fixed;
  inset: 0;
  z-index: 300;
  background: rgba(21, 21, 26, 0.92);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 3rem 1.5rem;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.25s ease;
}
.lightbox.is-open {
  opacity: 1;
  pointer-events: auto;
}
.lightbox img {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  display: block;
  cursor: default;
}
.lightbox video {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  display: none;
}
.lightbox iframe {
  display: none;
  width: 90vw;
  max-width: 800px;
  aspect-ratio: 16 / 9;
  height: auto;
  border: 0;
}
.lightbox iframe.is-vertical {
  aspect-ratio: 9 / 16;
  width: auto;
  height: min(85vh, 700px);
  max-width: 92vw;
}
.project-close.lightbox-close {
  position: fixed;
  top: 1.25rem;
  right: 1.25rem;
  margin-left: 0;
}
.lightbox-nav {
  position: fixed;
  top: 50%;
  transform: translateY(-50%);
  width: 48px;
  height: 48px;
  border-radius: 50%;
  background: var(--paper);
  border: 1px solid var(--rule);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  color: var(--ink);
  z-index: 1;
}
.lightbox-nav svg { width: 20px; height: 20px; }
.lightbox-nav:focus-visible {
  outline: 2px solid var(--ochre);
  outline-offset: 2px;
}
.lightbox-prev { left: 1.25rem; }
.lightbox-next { right: 1.25rem; }
@media (max-width: 600px) {
  .lightbox-nav { width: 40px; height: 40px; }
  .lightbox-prev { left: 0.6rem; }
  .lightbox-next { right: 0.6rem; }
}
/* Project popup prev/next — same treatment as the lightbox's nav
   arrows, just scoped to a different class so both can coexist (the
   lightbox can open on top of a project popup for its images). */
.project-nav {
  position: fixed;
  top: 50%;
  transform: translateY(-50%);
  width: 48px;
  height: 48px;
  border-radius: 50%;
  background: var(--paper);
  border: 1px solid var(--rule);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  color: var(--ink);
  z-index: 3;
}
.project-nav svg { width: 20px; height: 20px; }
.project-nav:focus-visible {
  outline: 2px solid var(--ochre);
  outline-offset: 2px;
}
.project-nav-prev { left: 1.25rem; }
.project-nav-next { right: 1.25rem; }
@media (max-width: 600px) {
  .project-nav { width: 40px; height: 40px; }
  .project-nav-prev { left: 0.6rem; }
  .project-nav-next { right: 0.6rem; }
}
@media (prefers-reduced-motion: reduce) {
  .lightbox {
    transition: none;
  }
}

/* Thud */
.thud-intro { max-width: none; }
.panel-strip {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 0.75rem;
  margin-top: 1.5rem;
}
.panel.placeholder {
  aspect-ratio: 3 / 4;
  border: 2px solid var(--ink);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.85rem;
  background: var(--paper);
  overflow: hidden;
}
.panel img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform 0.3s ease;
}
.panel:hover img {
  transform: scale(1.15);
}
@media (max-width: 700px) {
  .panel-strip { grid-template-columns: repeat(2, 1fr); }
}

/* About / freelance */
.about-section {
  position: relative;
  color: var(--paper);
  z-index: 2;
  padding-top: 8rem;
  padding-bottom: 8rem;
}
/* Decorative accent line — three separate SVG segments (one each in
   About, Contact, and the footer) positioned to read as one
   continuous line weaving through the page. Kept as separate
   sections rather than one SVG spanning all three so it doesn't
   require wrapping those sections in a shared parent, which risked
   breaking .about-texture's sticky behavior (sticky depends on its
   exact parent chain). Each section housing a segment needs
   position:relative for the absolute segment to anchor to it, and
   the segment must come before that section's real content in DOM
   order so it paints behind it (rather than using a z-index, which
   would need coordinating across every child instead). */
.accent-line {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  z-index: -1;
  overflow: visible;
  pointer-events: none;
}
.accent-line path {
  fill: none;
  stroke: var(--ochre);
  stroke-width: 18;
  stroke-linecap: round;
  vector-effect: non-scaling-stroke;
}
/* Full-bleed dark background via a pseudo-element instead of
   clip-path on the section itself — clip-path (and similarly
   transform/filter) on an ancestor is known to break how
   position:sticky calculates its scroll container for descendants,
   which is exactly what .about-lego-decoration below needs to work.
   Uses left/right with negative viewport-width offsets rather than
   left:50% + transform:translateX(-50%), avoiding a transform on
   this element (harmless now that background-attachment:fixed has
   been removed below, but no reason to reintroduce one). */
/* Full-bleed solid dark fill via a pseudo-element instead of
   clip-path on the section itself — clip-path (and similarly
   transform/filter) on an ancestor is known to break how
   position:sticky calculates its scroll container for descendants,
   which is exactly what .about-lego-decoration and .about-texture
   below need to work. Uses left/right with negative viewport-width
   offsets rather than left:50% + transform:translateX(-50%), so
   there's no transform on this element either. Just the solid color
   now — the halftone pattern lives on .about-texture below instead,
   where it can be sticky. */
.about-section::before {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  left: calc(-50vw + 50%);
  right: calc(-50vw + 50%);
  background-color: var(--ink);
  z-index: -2;
}
/* Sticky halftone texture — pins in place at the top of the viewport
   for as long as the section is scrolling past (up to its own
   height), giving the same "background stays still, content moves"
   parallax feel as background-attachment:fixed would, but without
   that property's severe, long-documented scroll-jank problems in
   Firefox. The negative margin-bottom keeps this from pushing the
   section's actual content down, since a sticky element still
   reserves its own space in normal flow otherwise. Full-bleed width
   uses width:100vw + margin-left (not left/right + calc, which is
   what .about-section::before above uses) — position:sticky resolves
   left/right against its parent's content box (inside the padding),
   not the padding box the way position:absolute does, so the
   left/right technique leaves this confined to roughly the text
   column's width instead of the full viewport. margin-left resolves
   the same way regardless of position type, which is why it's used
   here instead. */
.about-texture {
  position: sticky;
  top: 0;
  /* Height grown by the same 8rem that margin-top pulls this up by
     (see below) — otherwise the negative margin-top ALSO shrinks how
     much space this element reserves in normal flow, which quietly
     ate into .about-section's own padding-top and pulled the
     following content up flush against whatever comes before this
     section. Growing height by the same amount keeps the element's
     net contribution to page layout at zero: only its position shifts
     up, not how much room it takes. */
  height: calc(100vh + 8rem);
  /* Pulls this up to counteract .about-section's own padding-top:8rem
     — as a normal-flow child, this element's natural starting
     position is BELOW that padding, unlike the absolutely-positioned
     solid-fill layer below it, which ignores padding entirely and
     covers the full section including that top area. Without this,
     the top 8rem of the section shows solid ink with no texture. */
  margin-top: -8rem;
  margin-bottom: -100vh;
  width: 100vw;
  margin-left: calc(-50vw + 50%);
  /* TEMPORARY diagnostic scale — big, sparse dots to make the sticky
     effect obvious or not. Shrink back down once confirmed working.
     Two circles per tile (one at 25%/25%, one offset by half a tile
     at 75%/75%) creates the staggered/brick row pattern — each tile
     repeat lines the second dot up exactly between the first dot's
     repeats, both horizontally and vertically. */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100'%3E%3Ccircle cx='25' cy='25' r='2' fill='%23F3EEE4' fill-opacity='0.2'/%3E%3Ccircle cx='75' cy='75' r='2' fill='%23F3EEE4' fill-opacity='0.2'/%3E%3C/svg%3E");
  z-index: -1;
  pointer-events: none;
}
/* Fixed at the bottom of the section — no scroll animation, just
   anchored in place. */
.about-lego-decoration {
  position: absolute;
  bottom: 1rem;
  right: 0.5rem;
  width: 220px;
  pointer-events: none;
  z-index: 1;
}
@media (max-width: 900px) {
  .about-lego-decoration { width: 150px; right: 0.25rem; }
}
@media (max-width: 700px) {
  /* Not enough room next to the stacked mobile layout — hide rather
     than have it overlap the photo or text awkwardly. */
  .about-lego-decoration { display: none; }
}
.about-grid {
  display: grid;
  grid-template-columns: 560px 1fr;
  gap: 2.5rem;
  align-items: start;
}
.about-photo {
  width: 100%;
  aspect-ratio: 4 / 5;
  object-fit: cover;
  border: 1px solid var(--rule);
  display: block;
}
.about-copy h2 { margin-top: 0; }
.availability-label {
  color: var(--ochre);
  font-weight: 700;
}
.availability-highlight {
  background: var(--ochre);
  color: var(--ink);
  padding: 0.6rem 0.9rem;
  margin: 0 0 0.6rem;
}
.highlight-label {
  color: #FFFFFF;
  font-weight: 700;
}
@media (max-width: 700px) {
  .about-grid { grid-template-columns: 1fr; }
  .about-photo { max-width: 260px; margin: 0 auto; }
}
.about-section p, .freelance-section p { max-width: 65ch; }

/* Contact */
.contact-section {
  position: relative;
}
.contact-section a {
  font-family: var(--body);
  font-weight: 700;
  font-size: 1.4rem;
  text-decoration: underline;
  text-underline-offset: 4px;
}

/* Contact */
.contact-intro { margin-bottom: 2rem; }
.contact-intro a {
  font-family: var(--display);
  font-size: 1.15rem;
  text-decoration: underline;
  text-underline-offset: 4px;
}
.contact-form {
  max-width: 520px;
  display: flex;
  flex-direction: column;
  gap: 1.25rem;
}
.form-row {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}
.form-row label {
  font-size: 0.8rem;
  font-weight: 500;
}
.form-row input,
.form-row textarea {
  font-family: var(--body);
  font-size: 0.9rem;
  padding: 0.7rem 0.8rem;
  border: 1px solid var(--rule);
  background: #fff;
  color: var(--ink);
  border-radius: 0;
}
.form-row input:focus,
.form-row textarea:focus {
  outline: 2px solid var(--indigo);
  outline-offset: 1px;
}
.contact-form button {
  align-self: flex-start;
  cursor: pointer;
}
.form-status {
  margin: 0;
  font-size: 0.8rem;
  min-height: 1.2em;
}
.form-status-success { color: var(--indigo); }
.form-status-error { color: var(--red); }

/* Honeypot — hidden from sighted users and screen readers, but still
   present in the DOM/tab order removed, so bots that don't render CSS
   or that blindly fill every field will trip it. */
.honeypot {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

footer {
  position: relative;
  z-index: 2;
  padding: 0.5rem 1.5rem 0.6rem;
  font-size: 0.78rem;
  background: var(--ochre);
  color: var(--ink);
  text-align: center;
}

/* Keyboard focus visibility */
a:focus-visible, .cta:focus-visible {
  outline: 2px solid var(--ochre);
  outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
  .work-image img,
  .gallery-tile img,
  .gallery-tile video,
  .fun-project-image img,
  .fun-project-image video,
  .panel img {
    transition: none;
  }
  .work-item:hover .work-image img,
  .gallery-tile:hover img,
  .gallery-tile:hover video,
  .fun-project-tile:hover .fun-project-image img,
  .fun-project-tile:hover .fun-project-image video,
  .panel:hover img {
    transform: none;
  }
}
