/* ==========================================================================
   JAMES MERRY – MAIN STYLESHEET
   ==========================================================================
   Contents
     0.  Font files
     1.  Settings (colours, fonts, spacing) – change the look here
     2.  Base / reset
     3.  Accessibility helpers
     4.  Header and logo
     5.  Navigation (desktop + mobile)
     6.  Project grid (Motion / Illustration)
     7.  Project pages (video, details, gallery, Lottie)
     8.  Lightbox (enlarged gallery images)
     9.  General pages (titles, buttons, notices)
     10. About page
     11. Contact page and form
     12. Blog
     13. Footer
     14. Reduced motion
   ========================================================================== */


/* ==========================================================================
   0. FONT FILES
   Plus Jakarta Sans, hosted on this site (no Google Fonts request).
   It's a "variable" font: one file covers every weight from regular to bold.
   Split in two so browsers only download the characters a page needs.
   Licence: SIL Open Font License – see /assets/fonts/OFL.txt
   ========================================================================== */
@font-face {
  font-family: "Plus Jakarta Sans";
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;   /* show text straight away in a fallback font while it loads */
  src: url("../fonts/plus-jakarta-sans-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* Extra accented letters (e.g. ş, ł, ő) – only downloaded if a page uses them. */
@font-face {
  font-family: "Plus Jakarta Sans";
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url("../fonts/plus-jakarta-sans-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF;
}


/* ==========================================================================
   1. SETTINGS
   Change a value here and it updates everywhere it's used.
   Contrast ratios are against the background, for WCAG 2.2 AA
   (normal text needs at least 4.5:1).
   ========================================================================== */
:root {
  /* Colours */
  --color-bg:           #363636;   /* page background */
  --color-field:        #1f1f1f;   /* form field background, darker than the page */
  --color-field-edge:   #8a8a8a;   /* thin edge around form fields (needs 3:1 against the page) */
  --color-surface:      #2d2d2d;   /* placeholder behind images while they load */
  --color-text:         #f2f2f2;   /* main text (10.8:1) */
  --color-muted:        #c4c4c4;   /* dates, hints (6.9:1) */
  --color-accent:       #00ccff;   /* cyan: links, active menu item, focus rings (6.4:1) */
  --color-accent-hover: #8fe9ff;   /* lighter cyan for links on hover (8.8:1) */
  --color-line:         #5e5e5e;   /* dividers and borders */
  --color-error:        #ffa48c;   /* form errors (6.3:1) */
  --color-success:      #6fe0a0;   /* form success */

  /* Typography */
  --font: "Plus Jakarta Sans", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --text-base: 1rem;                              /* 16px */
  --text-nav:  1.25rem;                           /* 20px */
  --text-h1:   clamp(2rem, 1.4rem + 2.6vw, 2.9rem);  /* scales from ~32px to ~46px */
  --text-h2:   clamp(1.5rem, 1.2rem + 1.2vw, 2rem);

  /* Spacing */
  --gutter: clamp(16px, 3vw, 32px);   /* page edge padding: 16px on phones → 32px */
  --gap:    16px;                     /* space between grid tiles */

  /* Motion */
  --speed: 0.3s;
}


/* ==========================================================================
   2. BASE / RESET
   ========================================================================== */
*,
*::before,
*::after {
  box-sizing: border-box;
}

html {
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
  /* When jumping to a link on the same page (e.g. "Skip to content" or the
     contact form), stop below the fixed header instead of hiding behind it.
     --header-height is measured by main.js; 140px is a fallback. */
  scroll-padding-top: calc(var(--header-height, 140px) + 16px);
}

body {
  margin: 0;
  min-height: 100vh;
  display: flex;
  flex-direction: column;            /* keeps the footer at the bottom on short pages */
  background: var(--color-bg);
  color: var(--color-text);
  font-family: var(--font);
  font-size: var(--text-base);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

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

img {
  height: auto;
}

h1, h2, h3 {
  font-weight: 400;
  line-height: 1.2;
  margin: 0 0 0.75em;
}

h2 { font-size: var(--text-h2); }
h3 { font-size: 1.125rem; }

p {
  margin: 0 0 1rem;
}

a {
  color: var(--color-accent);
  text-underline-offset: 0.2em;
}

a:hover {
  color: var(--color-accent-hover);
  text-decoration-thickness: 2px;
}

hr {
  border: 0;
  border-top: 1px solid var(--color-line);
  margin: 2.5rem 0;
}

/* Main content area grows to fill the screen so the footer sits at the bottom. */
.site-main {
  flex: 1;
  width: 100%;
  padding: 0 var(--gutter);
}


/* ==========================================================================
   3. ACCESSIBILITY HELPERS
   ========================================================================== */

/* Clear focus ring for keyboard users on every interactive element. */
:focus-visible {
  outline: 3px solid var(--color-accent);
  outline-offset: 3px;
}

/* Hide something visually but keep it for screen readers. */
.visually-hidden {
  position: absolute !important;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* "Skip to content" link: hidden until it receives keyboard focus. */
.skip-link {
  position: absolute;
  top: 8px;
  left: 8px;
  z-index: 1000;
  padding: 0.75rem 1rem;
  background: var(--color-text);
  color: var(--color-bg);
  font-weight: 700;
  transform: translateY(-200%);
}

.skip-link:focus {
  transform: none;
}


/* ==========================================================================
   4. HEADER AND LOGO
   ========================================================================== */
/* The header stays fixed at the top of the window while the page scrolls.
   It has a solid background so content passes cleanly underneath it.
   Once the page scrolls it shrinks to a slimmer bar (see "is-scrolled" below). */
.site-header {
  /* Sizes – full header and slim (scrolled) header */
  --logo-width:        100px;
  --logo-width-slim:   60px;
  --header-pad-slim:   12px;   /* space above and below the logo when slim */

  position: sticky;
  top: 0;
  z-index: 40;              /* above page content (carousel tiles, edges, etc.) */
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.5rem;
  padding: var(--gutter);
  padding-bottom: 2rem;
  background: var(--color-bg);
  transition: padding var(--speed) ease;
}

@media (max-width: 767px) {
  .site-header {
    --logo-width:      76px;
    --logo-width-slim: 50px;
    --header-pad-slim: 8px;
  }
}

/* Slim header once the page has scrolled. main.js adds a matching space
   below it while it's slim, so the page content doesn't jump up. */
.site-header.is-scrolled {
  padding-top: var(--header-pad-slim);
  padding-bottom: var(--header-pad-slim);
}

/* Used by main.js while measuring the header – switches animations off briefly. */
.site-header.no-transition,
.site-header.no-transition * {
  transition: none !important;
}

/* Soft fade below the header, so content melts away as it scrolls underneath
   instead of being cut off by a hard line. Only shown once the page has been
   scrolled (main.js adds "is-scrolled"), so at the top it never dims the
   start of the page. Change the height to make the fade longer or shorter. */
.site-header::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 100%;
  height: 48px;
  background: linear-gradient(to bottom, var(--color-bg), transparent);
  pointer-events: none;     /* clicks pass straight through to the content */
  opacity: 0;
  transition: opacity var(--speed);
}

.site-header.is-scrolled::after {
  opacity: 1;
}

.site-logo {
  position: relative;
  z-index: 60;              /* stays above the open mobile menu */
  flex-shrink: 0;
  /* The logo images have a solid black background. "screen" blending makes
     black see-through and keeps the white letters, so the logo sits cleanly
     on any background colour without editing the GIF. */
  mix-blend-mode: screen;
}

.site-logo img {
  width: var(--logo-width);
  height: auto;
  transition: width var(--speed) ease;
}

.site-header.is-scrolled .site-logo img {
  width: var(--logo-width-slim);
}


/* ==========================================================================
   5. NAVIGATION
   Desktop: a row of links. Below 768px: a "Menu" button opens a full-screen menu.
   ========================================================================== */
.nav-menu {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem 1.6rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

.nav-menu__link {
  position: relative;            /* the underline below is positioned against this */
  display: inline-block;
  padding: 4px 0;
  color: #ffffff;
  font-size: var(--text-nav);
  text-decoration: none;
  transition: color var(--speed);
}

/* The underline: grows out from the centre on hover, with a little bounce.
   It starts at zero width (scaleX(0)) and springs to full width, overshooting
   slightly before settling. To change the bounce, edit the cubic-bezier:
   the 1.56 is the overshoot – lower it for less bounce, 1 for none. */
.nav-menu__link::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1px;
  background: currentColor;      /* matches the link colour */
  transform: scaleX(0);
  transform-origin: center;
  transition: transform 0.45s cubic-bezier(0.34, 1.56, 0.64, 1);
}

.nav-menu__link:hover,
.nav-menu__link:focus-visible {
  color: var(--color-accent-hover);
}

.nav-menu__link:hover::after,
.nav-menu__link:focus-visible::after {
  transform: scaleX(1);
}

/* The current page: accent colour, with its underline always showing. */
.nav-menu__link[aria-current="page"] {
  color: var(--color-accent);
}

.nav-menu__link[aria-current="page"]::after {
  transform: scaleX(1);
}

/* Menu button – hidden on larger screens. */
.nav-toggle {
  display: none;
}

@media (max-width: 767px) {
  .nav-toggle {
    position: relative;
    z-index: 60;            /* stays above the open menu so it can close it */
    display: inline-flex;
    align-items: center;
    gap: 0.6rem;
    min-height: 44px;       /* comfortable touch target */
    padding: 0.5rem 0;
    background: none;
    border: 0;
    color: var(--color-text);
    font: inherit;
    font-size: 1.125rem;
    cursor: pointer;
  }

  /* Three-line "burger" icon drawn with CSS; turns into an X when open. */
  .nav-toggle__icon,
  .nav-toggle__icon::before,
  .nav-toggle__icon::after {
    display: block;
    width: 22px;
    height: 2px;
    background: currentColor;
    transition: transform var(--speed), background-color var(--speed);
  }

  .nav-toggle__icon {
    position: relative;
  }

  .nav-toggle__icon::before,
  .nav-toggle__icon::after {
    content: "";
    position: absolute;
    left: 0;
  }

  .nav-toggle__icon::before { top: -7px; }
  .nav-toggle__icon::after  { top: 7px; }

  .nav-toggle[aria-expanded="true"] .nav-toggle__icon {
    background: transparent;
  }
  .nav-toggle[aria-expanded="true"] .nav-toggle__icon::before {
    transform: translateY(7px) rotate(45deg);
  }
  .nav-toggle[aria-expanded="true"] .nav-toggle__icon::after {
    transform: translateY(-7px) rotate(-45deg);
  }

  /* The menu itself: a full-screen black panel. */
  .nav-menu {
    position: fixed;
    inset: 0;
    z-index: 50;
    flex-direction: column;
    justify-content: center;
    gap: 0.5rem;
    padding: 6rem var(--gutter) 2rem;
    background: var(--color-bg);
    visibility: hidden;
    opacity: 0;
    transition: opacity var(--speed), visibility var(--speed);
  }

  .nav-menu.is-open {
    visibility: visible;
    opacity: 1;
  }

  .nav-menu__link {
    font-size: 2rem;
  }

  /* Stop the page behind scrolling while the menu is open. */
  body.menu-open {
    overflow: hidden;
  }
}


/* ==========================================================================
   6. PROJECT GRID
   1 column on phones, 2 on tablets, 4 on desktops – all square tiles.
   ========================================================================== */
.project-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--gap);
  margin: 0;
  padding: 0;
  list-style: none;
}

@media (min-width: 640px) {
  .project-grid { grid-template-columns: repeat(2, 1fr); }
}

@media (min-width: 1024px) {
  .project-grid { grid-template-columns: repeat(4, 1fr); }
}

.card {
  position: relative;
  aspect-ratio: 1 / 1;
  overflow: hidden;
  /* No background here: at the softened edges of a scaled tile it would
     bleed through as a faint line. The placeholder colour is on the image. */
}

.card__link {
  display: block;
  width: 100%;
  height: 100%;
  color: #fff;
  text-decoration: none;
}

/* Keep the focus ring inside the tile so it isn't cut off. */
.card__link:focus-visible {
  outline-offset: -3px;
}

.card__image,
.card__hover {
  position: absolute;
  /* 1px past the tile's edges, for the same reason as the gradient below:
     no hairline of background can show when a tile pops. */
  inset: -1px;
  width: calc(100% + 2px);
  height: calc(100% + 2px);
  max-width: none;          /* the site-wide img rule would otherwise cap this at 100% */
  object-fit: cover;
}

/* Placeholder colour while the thumbnail loads. */
.card__image {
  background: var(--color-surface);
}

/* The hover clip (video or animated image) is added by main.js and fades in. */
.card__hover {
  opacity: 0;
  transition: opacity var(--speed);
  pointer-events: none;
}

.card.is-playing .card__hover {
  opacity: 1;
}

/* Dark gradient across the bottom of every tile, always visible, over both
   the still image and the hover video – so thumbnails don't need one baked in.
   It also keeps the hover title readable on light images.
     --card-shade-height:   how far up the tile it reaches
     --card-shade-strength: how dark it is at the very bottom (0–1)
   The extra colour stops make the fade smooth, without visible banding. */
.card {
  --card-shade-height: 40%;
  --card-shade-strength: 0.75;
}

.card__link::after {
  content: "";
  position: absolute;
  /* 1px past the tile's edges: when a tile pops (scales up) its edges land
     between whole pixels, and without this a hairline gap can appear between
     the gradient and the image. The tile clips the overhang, so it's invisible. */
  left: -1px;
  right: -1px;
  bottom: -1px;
  z-index: 1;                          /* above the image and video, below the title */
  height: var(--card-shade-height);
  background: linear-gradient(to top,
    rgba(0, 0, 0, var(--card-shade-strength)) 0%,
    rgba(0, 0, 0, calc(var(--card-shade-strength) * 0.6)) 35%,
    rgba(0, 0, 0, calc(var(--card-shade-strength) * 0.2)) 70%,
    rgba(0, 0, 0, 0) 100%);
  pointer-events: none;
}

/* Project title: slides up over the gradient on hover or keyboard focus. */
.card__title {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 2;
  padding: 2.5rem 1rem 0.9rem;
  font-size: 1rem;
  line-height: 1.3;
  opacity: 0;
  transform: translateY(8px);
  transition: opacity var(--speed), transform var(--speed);
}

.card__link:hover .card__title,
.card__link:focus-visible .card__title {
  opacity: 1;
  transform: none;
}

/* Touch screens have no hover, so always show the title there. */
@media (hover: none) {
  .card__title {
    opacity: 1;
    transform: none;
  }
}


/* --------------------------------------------------------------------------
   6b. CAROUSEL (desktops with a mouse only)
   main.js adds the "carousel--active" class on wide screens with a mouse,
   which turns the grid above into a single scrolling row. Phones, tablets
   and touchscreens keep the grid.

   --carousel-visible: how many tiles fit across the screen. A fraction
   (e.g. 3.5) leaves part of the next tile peeking in, a hint there's more.
   -------------------------------------------------------------------------- */
.carousel {
  --carousel-visible: 3.5;
  --carousel-edge: 120px;            /* width of the left/right hover zones */
  --thumb-size: 400px;               /* largest a tile ever gets on screen, including the hover pop
                                        (the thumbnails themselves are 600×600, so never stretched) */
  /* Largest resting tile: small enough that even the hover pop (--pop-scale)
     tops out at --thumb-size. Wide screens show more tiles instead of bigger ones. */
  --carousel-tile-max: calc(var(--thumb-size) / var(--pop-scale));

  /* Hover "pop" settings */
  --pop-scale: 1.08;                 /* how much a hovered tile grows (1.08 = 8% bigger) */
  --pop-speed: 0.45s;                /* how long the pop takes */
  --pop-delay: 0.05s;                /* time offset: neighbours start moving this much later… */
  --pop-delay-far: 0.1s;             /* …and tiles further away later still */
  --pop-room: 24px;                  /* space kept clear at the ends of the row while a tile is popped */
  --pop-room-y: 48px;                /* space above/below the row, so the pop and its shadow are never cut off */
  /* A "springy" curve: overshoots slightly, then settles. */
  --pop-ease: cubic-bezier(0.34, 1.56, 0.64, 1);

  position: relative;
}

/* Centre the row vertically in the space between header and footer. */
.site-main:has(.carousel--active) {
  display: flex;
  flex-direction: column;
  justify-content: center;
}

.carousel--active .project-grid {
  display: flex;
  gap: var(--gap);
  overflow-x: auto;
  overscroll-behavior-x: contain;    /* don't trigger the browser's "back" swipe */
  scrollbar-width: thin;
  scrollbar-color: var(--color-line) transparent;
  /* Breathing room so a popped tile isn't clipped at the top, bottom or ends.
     The negative margin cancels it out, so the row still lines up with the header. */
  padding: var(--pop-room-y) var(--pop-room) calc(var(--pop-room-y) + 12px);   /* +12px for the scrollbar */
  margin: calc(var(--pop-room-y) * -1) calc(var(--pop-room) * -1) 0;
}

.carousel--active .card {
  /* Tile width: fit --carousel-visible tiles across, but never taller than
     the screen leaves room for (so the whole row fits without page scrolling),
     and never bigger than the thumbnails' own size (--carousel-tile-max). */
  flex: 0 0 min(
    calc((100% - var(--gap) * (var(--carousel-visible) - 1)) / var(--carousel-visible)),
    calc(100vh - 240px),
    var(--carousel-tile-max)
  );
  /* Every tile animates its movement; the delay is set per tile below.
     The shadow fades in smoothly (no bounce). */
  transition:
    transform var(--pop-speed) var(--pop-ease),
    box-shadow var(--pop-speed) ease-out;
  box-shadow: 0 0 0 transparent, 0 0 0 transparent;   /* "no shadow", in a form that can animate */
  /* Draw each tile (image, gradient, title) as one layer that the browser
     scales as a single picture. Without this the layers are redrawn
     separately on every frame of the pop, so their edges can round
     differently – causing a flickering hairline at the edge. */
  will-change: transform;
  backface-visibility: hidden;
}

/* --- Hover pop ------------------------------------------------------------
   The hovered tile grows; the tiles around it slide outwards to make room.
   They slide by half the extra size of the popped tile, plus a little gap
   (the 50% here means "half of a tile's own width").
   Uses :has() so no JavaScript is needed. */
.carousel--active .card:hover {
  z-index: 3;                        /* sits above its neighbours while growing */
  transform: scale(var(--pop-scale));
  /* Two soft layers: a close, darker one and a wide, faint one. The negative
     spread pulls both in at the sides so the shadow falls mostly downwards and
     fades out before reaching the edge of the row (no hard cut-off line).
     If you make these bigger, increase --pop-room-y to match. */
  box-shadow:
    0 8px 16px -6px rgba(0, 0, 0, 0.35),
    0 22px 34px -18px rgba(0, 0, 0, 0.4);
}

/* Tiles AFTER the hovered one move right… */
.carousel--active .card:hover ~ .card {
  transform: translateX(calc((var(--pop-scale) - 1) * 50% + var(--gap) / 2));
  transition-delay: var(--pop-delay-far);
}

/* …and tiles BEFORE it move left. */
.carousel--active .card:has(~ .card:hover) {
  transform: translateX(calc((var(--pop-scale) - 1) * -50% - var(--gap) / 2));
  transition-delay: var(--pop-delay-far);
}

/* The two direct neighbours go first, just after the pop starts: that's the time offset. */
.carousel--active .card:hover + .card,
.carousel--active .card:has(+ .card:hover) {
  transition-delay: var(--pop-delay);
}

/* No pop for visitors who prefer reduced motion. */
@media (prefers-reduced-motion: reduce) {
  .carousel--active .card,
  .carousel--active .card:hover,
  .carousel--active .card:hover ~ .card,
  .carousel--active .card:has(~ .card:hover) {
    transform: none;
    box-shadow: none;
  }
}

/* Left/right hover zones. Resting the cursor here scrolls the row;
   clicking jumps one screen. Created by main.js. */
.carousel__edge {
  position: absolute;
  top: calc(var(--pop-room-y) * -1);        /* cover popped tiles too */
  bottom: calc(var(--pop-room-y) + 12px); /* stop above the scrollbar so it can still be dragged */
  z-index: 5;
  display: flex;
  align-items: center;
  width: var(--carousel-edge);
  padding: 0;
  border: 0;
  color: var(--color-text);
  font-size: 2rem;
  cursor: pointer;
  opacity: 0;
  pointer-events: none;              /* hidden zones don't block clicks */
  transition: opacity var(--speed);
}

/* The row reaches --pop-room past the carousel on each side (the space that
   stops popped tiles being clipped), so the zones extend out to match. */
.carousel__edge--prev {
  left: calc(var(--pop-room) * -1);
  justify-content: flex-start;
  padding-left: 1rem;
  background: linear-gradient(to right, color-mix(in srgb, var(--color-bg) 90%, transparent), transparent);
}

.carousel__edge--next {
  right: calc(var(--pop-room) * -1);
  justify-content: flex-end;
  padding-right: 1rem;
  background: linear-gradient(to left, color-mix(in srgb, var(--color-bg) 90%, transparent), transparent);
}

/* Only show a zone when there's more to see in that direction. */
.carousel.can-prev .carousel__edge--prev,
.carousel.can-next .carousel__edge--next {
  opacity: 0.6;
  pointer-events: auto;
}

.carousel.can-prev .carousel__edge--prev:hover,
.carousel.can-next .carousel__edge--next:hover {
  opacity: 1;
  color: var(--color-accent-hover);
}

/* The arrow inside each zone: /assets/icons/arrow.svg, which points left.
   The arrow takes the text colour (white, light cyan on hover); its circle
   matches the page background. */
.carousel__arrow {
  --nudge: -8px;                     /* how far a click nudges it (left for "previous") */
  display: grid;
  place-items: center;
  width: 48px;
  height: 48px;
  line-height: 1;
  /* Hover bounce: springs up a little bigger, overshoots, settles. */
  transition: scale 0.4s cubic-bezier(0.34, 1.8, 0.64, 1);
}

.carousel__arrow svg {
  display: block;
  width: 100%;
  height: 100%;
}

/* The right-hand arrow is the same drawing, mirrored, and nudges right. */
.carousel__edge--next .carousel__arrow {
  --nudge: 8px;
}

.carousel__edge--next .carousel__arrow svg {
  transform: scaleX(-1);
}

.carousel__edge:hover .carousel__arrow {
  scale: 1.15;
}

/* Click bounce: a quick push in the arrow's direction that springs back,
   wobbling once on the way. Added by main.js on each click. */
.carousel__arrow.is-nudged {
  animation: arrow-nudge 0.5s ease-out;
}

@keyframes arrow-nudge {
  0%   { translate: 0 0; }
  30%  { translate: var(--nudge) 0; }
  60%  { translate: calc(var(--nudge) * -0.35) 0; }
  80%  { translate: calc(var(--nudge) * 0.12) 0; }
  100% { translate: 0 0; }
}


/* The mini carousel (added by main.js) replaces the scrollbar, so hide it.
   The row still scrolls with the wheel, edges, keyboard and trackpad. */
.carousel--active .project-grid {
  scrollbar-width: none;
}

.carousel--active .project-grid::-webkit-scrollbar {
  display: none;
}

/* Mini carousel: a strip of small thumbnails below the big carousel, with a
   box showing which part is on screen. Click to jump there; drag the box to
   scrub. main.js sizes the thumbnails (--mini-size) to fit the screen. */
.carousel-minimap {
  display: flex;
  justify-content: center;
  /* Pull it up into the empty space the carousel keeps below the tiles for
     the hover pop (--pop-room-y in the carousel settings above, 48px + 12px). */
  margin-top: -28px;
  position: relative;
  z-index: 2;                        /* above any popped tile's shadow */
}

.carousel-minimap[hidden] {
  display: none;
}

.carousel-minimap__strip {
  position: relative;
  display: flex;
  gap: 2px;
  padding: 4px;                      /* room for the box's border around the edge thumbs */
  cursor: pointer;
  touch-action: none;                /* lets drag-to-scrub work with a pen or touch screen */
  user-select: none;
}

.carousel-minimap {
  --mini-faded: 0.6;                 /* opacity of thumbnails outside the box (1 = no fade) */
  --mini-box-pop: 1.08;              /* how much the box grows on hover */
}

.carousel-minimap__thumb {
  display: block;
  width: var(--mini-size, 48px);
  height: var(--mini-size, 48px);
  object-fit: cover;
  background: var(--color-surface);
  pointer-events: none;              /* clicks go to the strip */
  /* Faded outside the box, full strength inside. main.js sets --inside
     (0 = fully outside, 1 = fully inside) as the carousel scrolls. */
  opacity: calc(var(--mini-faded) + (1 - var(--mini-faded)) * var(--inside, 1));
}

/* The "you are here" box. main.js sets its position and width. */
.carousel-minimap__box {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  border: 2px solid var(--color-accent);
  border-radius: 6px;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.35);   /* keeps it visible over light thumbnails */
  cursor: grab;                      /* hints that the box can be dragged */
  /* Hover bounce: springs up a little bigger, overshoots, settles. */
  transition: scale 0.4s cubic-bezier(0.34, 1.8, 0.64, 1), border-color var(--speed);
}

/* Pops only when the cursor is on the box itself (or while dragging). */
.carousel-minimap__box:hover,
.carousel-minimap__strip.is-pressed .carousel-minimap__box {
  scale: var(--mini-box-pop);
  border-color: var(--color-accent-hover);
}

.carousel-minimap__strip.is-pressed .carousel-minimap__box {
  cursor: grabbing;
}


/* --------------------------------------------------------------------------
   6c. FLOATING BACKGROUND ICONS (desktop carousel pages only)
   The band of drifting line drawings below the carousel. main.js sets the
   band's top edge (--icon-field-top), each icon's position, size and
   faintness, and only shows the band when there's enough room.
   The icon list lives in /data/background_icons.php
   -------------------------------------------------------------------------- */
.icon-field {
  position: fixed;
  left: 0;
  right: 0;
  top: var(--icon-field-top, 100vh);
  bottom: 0;
  z-index: 1;
  overflow: hidden;
  pointer-events: none;            /* only the icons themselves are clickable */
}

.icon-field[hidden] {
  display: none;
}

.icon-field__icon {
  position: absolute;
  left: 0;
  top: 0;
  display: block;
  color: var(--color-text);
  opacity: var(--icon-opacity, 0.5);   /* fainter when further away (set by main.js) */
  pointer-events: auto;
  transform-origin: 0 0;               /* main.js positions and scales from the top-left */
  will-change: transform;
  transition: color var(--speed), opacity var(--speed);
}

.icon-field__icon svg {
  display: block;
  height: var(--icon-height, 60px);
  width: auto;
}

/* Hover: full strength in the accent colour. */
.icon-field__icon:hover {
  color: var(--color-accent);
  opacity: 1;
}

/* Hover animation loops: the frames (added by main.js on first hover) take
   the resting drawing's place while the loop plays. Both share the same
   crop, so the swap is seamless. */
.icon-field__anim {
  display: none !important;
}

.icon-field__icon.is-animating > svg {
  display: none;
}

.icon-field__icon.is-animating > .icon-field__anim {
  display: block !important;
}


/* ==========================================================================
   7. PROJECT PAGES
   ========================================================================== */
.project {
  padding-bottom: 2rem;
}

/* Responsive 16:9 video. */
.video-embed {
  position: relative;
  aspect-ratio: 16 / 9;
  background: var(--color-surface);
  margin-bottom: 2rem;
}

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

.project-title {
  font-size: var(--text-h1);
  margin: 0 0 2rem;
}

.project-subheading {
  margin-top: 2rem;
}

/* Details: Client / Roles / Tools etc. – 3 columns on larger screens. */
.project-info {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0 2rem;
  padding: 1.5rem 0 0.5rem;
  border-top: 1px solid var(--color-line);
}

@media (min-width: 768px) {
  .project-info {
    grid-template-columns: repeat(3, 1fr);
  }
}

.project-info strong {
  display: inline-block;
  margin-bottom: 0.15rem;
}

/* Image gallery: square tiles, 1 / 2 / 3 columns. */
.gallery {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--gap);
  margin: 1.5rem 0 2rem;
}

@media (min-width: 640px) {
  .gallery { grid-template-columns: repeat(2, 1fr); }
}

@media (min-width: 1024px) {
  .gallery { grid-template-columns: repeat(3, 1fr); }
}

.gallery__item {
  display: block;
  aspect-ratio: 1 / 1;
  overflow: hidden;
  background: var(--color-surface);
  cursor: zoom-in;
}

.gallery__item:focus-visible {
  outline-offset: -3px;
}

.gallery__item img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 0.5s ease;
}

.gallery__item:hover img {
  transform: scale(1.04);
}

/* Lottie animations: 2 per row on phones, 4 on desktops. */
.lottie-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--gap);
  margin: 1.5rem 0 2rem;
}

@media (min-width: 1024px) {
  .lottie-grid { grid-template-columns: repeat(4, 1fr); }
}

.lottie-grid dotlottie-player {
  display: block;
  aspect-ratio: 1 / 1;
  width: 100%;
  height: auto;
}

/* Previous / next project links at the bottom of each project page.
   Side by side on larger screens, stacked on phones. */
.project-pager {
  display: grid;
  grid-template-columns: 1fr;
  gap: 1rem 2rem;
  padding-top: 1.5rem;
  border-top: 1px solid var(--color-line);
}

@media (min-width: 640px) {
  .project-pager {
    grid-template-columns: 1fr 1fr;
  }
}

.project-pager__link {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  padding: 0.5rem 0;
  color: var(--color-text);
  text-decoration: none;
}

/* "Next" sits on the right, even when there's no "Previous". */
.project-pager__link--next {
  grid-column: -2;
  text-align: right;
}

@media (max-width: 639px) {
  .project-pager__link--next {
    grid-column: auto;
  }
}

.project-pager__label {
  color: var(--color-muted);
  font-size: 0.875rem;
}

.project-pager__title {
  font-size: 1.375rem;
  line-height: 1.3;
  color: var(--color-accent);
  transition: color var(--speed);
}

.project-pager__link:hover .project-pager__title {
  color: var(--color-accent-hover);
}


/* ==========================================================================
   8. LIGHTBOX
   Built by main.js using the native <dialog> element.
   ========================================================================== */
.lightbox {
  width: 100vw;
  height: 100vh;
  max-width: none;
  max-height: none;
  margin: 0;
  padding: 0;
  border: 0;
  background: rgba(0, 0, 0, 0.95);
  color: var(--color-text);
}

.lightbox::backdrop {
  background: rgba(0, 0, 0, 0.95);
}

.lightbox__figure {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  margin: 0;
  padding: 4rem 4.5rem;
}

.lightbox__image {
  max-width: 100%;
  max-height: 100%;
  width: auto;
  height: auto;
  object-fit: contain;
}

.lightbox__count {
  position: absolute;
  left: var(--gutter);
  top: 1.25rem;
  margin: 0;
  color: var(--color-muted);
}

.lightbox__button {
  position: absolute;
  display: grid;
  place-items: center;
  width: 48px;
  height: 48px;
  padding: 0;
  background: rgba(0, 0, 0, 0.6);
  border: 1px solid var(--color-line);
  border-radius: 50%;
  color: var(--color-text);
  font: inherit;
  font-size: 1.5rem;
  line-height: 1;
  cursor: pointer;
  transition: background-color var(--speed), border-color var(--speed);
}

.lightbox__button:hover {
  background: var(--color-accent);
  border-color: var(--color-accent);
  color: #000;              /* dark arrow stays readable on the bright cyan */
}

.lightbox__close { top: 0.75rem; right: var(--gutter); }
.lightbox__prev  { left: 0.75rem;  top: 50%; transform: translateY(-50%); }
.lightbox__next  { right: 0.75rem; top: 50%; transform: translateY(-50%); }

@media (max-width: 639px) {
  .lightbox__figure { padding: 4rem 0.5rem 5rem; }
  .lightbox__prev,
  .lightbox__next { top: auto; bottom: 1rem; transform: none; }
  .lightbox__prev { left: calc(50% - 60px); }
  .lightbox__next { right: calc(50% - 60px); }
}


/* ==========================================================================
   9. GENERAL PAGES
   ========================================================================== */
.page-title {
  font-size: var(--text-h1);
  margin: 0 0 1.5rem;
}

/* A comfortable reading width for text-heavy pages. */
.page-narrow {
  max-width: 46rem;
}

/* Long-form text pages, such as the privacy notice. */
.prose {
  line-height: 1.65;
  padding-bottom: 1rem;
}

.prose h2 {
  font-size: 1.375rem;
  margin-top: 2.25rem;
}

.prose ul {
  padding-left: 1.25rem;
  margin: 0 0 1rem;
}

.prose li {
  margin-bottom: 0.5rem;
}

.prose code {
  font-size: 0.9em;
}

.prose__meta {
  color: var(--color-muted);
  margin-top: -0.75rem;
}

/* Pill-shaped button, used for "Download CV" and "Send". */
.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  padding: 0.6rem 1.5rem;
  background: transparent;
  border: 1px solid var(--color-text);
  border-radius: 999px;
  color: var(--color-text);
  font: inherit;
  font-size: 0.95rem;
  text-decoration: none;
  cursor: pointer;
  transition: background-color var(--speed), border-color var(--speed), color var(--speed);
}

.button:hover {
  background: var(--color-accent);
  border-color: var(--color-accent);
  color: #000;
}

/* Lists without bullets (awards, education). */
.plain-list {
  margin: 0 0 1rem;
  padding: 0;
  list-style: none;
}

.plain-list li {
  margin-bottom: 0.75rem;
}

/* Success / error message boxes. */
.notice {
  margin: 0 0 1.5rem;
  padding: 1rem 1.25rem;
  border: 1px solid;
  border-left-width: 4px;
}

.notice p:last-child,
.notice ul:last-child {
  margin-bottom: 0;
}

.notice--success { border-color: var(--color-success); }
.notice--error   { border-color: var(--color-error); }
.notice--error a { color: var(--color-error); }


/* ==========================================================================
   10. ABOUT PAGE
   ========================================================================== */
.about {
  display: grid;
  grid-template-columns: 1fr;
  gap: 2rem;
  padding-bottom: 2rem;
}

@media (min-width: 900px) {
  .about {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: 3rem;
    align-items: start;
  }

  /* Photo stays in view while scrolling the long text column. It stops just
     below the fixed header and its 48px fade (--header-height is measured
     by main.js and follows the header as it shrinks). */
  .about__photo {
    position: sticky;
    top: calc(var(--header-height, 140px) + 48px);
    transition: top var(--speed) ease;
  }
}

.about__text {
  max-width: 40rem;
}

/* The "Hello" that flicks through languages (animated by main.js, section 9).
   inline-block lets its width animate as words change; clip-path trims any
   letters that spill sideways mid-change, while still letting each word
   slide up from below. */
.hello {
  display: inline-block;
  white-space: nowrap;
  clip-path: inset(-50% 0 -50% 0);
}

/* "! I'm James." – leans slightly as it's pulled along, then straightens
   (main.js). Pivots from its bottom-left corner, like it's being dragged. */
.hello-rest {
  display: inline-block;
  transform-origin: left bottom;
}

.about__photo img {
  width: 100%;
  max-width: 420px;
}

/* Job titles: bold, normal size, so they read like the old site. */
.about__role {
  font-size: 1rem;
  font-weight: 700;
  line-height: 1.5;
  margin: 1.5rem 0 0.5rem;
}

/* Skills: icon on the left, heading and list beside it.
   Two groups per row on wider screens, one per row on phones. */
.skills {
  display: grid;
  grid-template-columns: 1fr;
  gap: 2rem;
  margin-top: 0.5rem;
}

@media (min-width: 600px) {
  .skills { grid-template-columns: 1fr 1fr; }
}

.skill {
  display: flex;
  align-items: flex-start;
  gap: 1rem;
}

.skill__icon {
  flex-shrink: 0;
  width: 40px;
  height: auto;
  margin-top: 0.15rem;          /* lines the icon up with the heading */
  color: var(--color-text);
}

.skill__title {
  font-size: 1rem;
  font-weight: 700;
  margin: 0 0 0.35rem;
}

.skill__list li {
  margin-bottom: 0.2rem;
}

.skill__text {
  margin: 0;
}


/* ==========================================================================
   11. CONTACT PAGE AND FORM
   ========================================================================== */
.contact-links {
  margin: 0 0 2.5rem;
  padding: 0;
  list-style: none;
}

.contact-links li {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin-bottom: 0.5rem;
}

.contact-links a {
  display: inline-block;
  padding: 0.25rem 0;        /* bigger tap area */
}

.icon {
  flex-shrink: 0;
  fill: currentColor;
}

.contact__form-heading {
  font-size: 1.5rem;
}

.field {
  margin-bottom: 1.5rem;
}

.field label {
  display: block;
  margin-bottom: 0.4rem;
  font-weight: 700;
}

.field__hint {
  font-weight: 400;
  color: var(--color-muted);
}

.field input,
.field textarea {
  display: block;
  width: 100%;
  padding: 0.75rem;
  background: var(--color-field);             /* darker grey than the page */
  /* Thin grey edge. Accessibility (WCAG 2.2, 1.4.11) asks for a form field's
     edge to stand out at least 3:1 from the page so everyone can see where
     the box is; a dark fill on a dark page can't reach that by itself. */
  border: 1px solid var(--color-field-edge);
  border-radius: 6px;                         /* softly rounded corners */
  color: var(--color-text);
  font: inherit;
  transition: background-color var(--speed), border-color var(--speed);
}

.field textarea {
  resize: vertical;
  min-height: 10rem;
}

/* A touch lighter on hover, as a hint the field can be clicked. */
.field input:hover,
.field textarea:hover {
  background: color-mix(in srgb, var(--color-field) 90%, var(--color-text));
}

/* While typing: the standard cyan focus ring (set in section 3), just
   offset a little so it sits clear of the field. */
.field input:focus-visible,
.field textarea:focus-visible {
  outline-offset: 2px;
}

/* Stop browsers painting autofilled fields white/yellow, which would hide the text. */
.field input:-webkit-autofill {
  -webkit-text-fill-color: var(--color-text);
  box-shadow: 0 0 0 100px var(--color-field) inset;
  caret-color: var(--color-text);
}

/* Fields with a problem get a thicker coral border (the inner shadow
   doubles the 1px border to 2px without changing the field's size). */
.field [aria-invalid="true"] {
  border-color: var(--color-error);
  box-shadow: inset 0 0 0 1px var(--color-error);
}

.field__error {
  margin: 0 0 0.4rem;
  color: var(--color-error);
  font-weight: 700;
}

/* "How your details are used" line above the Send button. */
.contact__privacy {
  color: var(--color-muted);
  font-size: 0.9rem;
}

/* Honeypot spam trap – moved off-screen (not display:none, which some bots skip). */
.field--trap {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}


/* ==========================================================================
   12. BLOG
   ========================================================================== */

/* Listing: cards in a responsive grid. */
.post-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 300px), 1fr));
  gap: 2.5rem var(--gap);
  margin: 0 0 2rem;
  padding: 0;
  list-style: none;
}

.post-card {
  position: relative;        /* lets the title link cover the whole card */
}

.post-card__image {
  width: 100%;
  aspect-ratio: 3 / 2;
  object-fit: cover;
  margin-bottom: 1rem;
  background: var(--color-surface);
  transition: opacity var(--speed);
}

.post-card:hover .post-card__image {
  opacity: 0.85;
}

.post-card__date {
  display: block;
  margin-bottom: 0.35rem;
  color: var(--color-muted);
  font-size: 0.875rem;
}

.post-card__title {
  font-size: 1.375rem;
  margin-bottom: 0.5rem;
}

.post-card__link {
  color: var(--color-text);
  text-decoration: none;
}

/* Stretch the link over the whole card so anywhere is clickable. */
.post-card__link::after {
  content: "";
  position: absolute;
  inset: 0;
}

.post-card:hover .post-card__link {
  color: var(--color-accent-hover);
}

.post-card:focus-within {
  outline: 3px solid var(--color-accent);
  outline-offset: 6px;
}

.post-card__link:focus-visible {
  outline: none;             /* the whole card shows the focus ring instead */
}

.post-card__excerpt {
  margin: 0;
  color: var(--color-muted);
}

/* Single post. */
.post {
  max-width: 46rem;
  margin: 0 auto;
  padding-bottom: 2rem;
}

.post__back {
  margin-bottom: 1.5rem;
}

.post__date {
  display: block;
  margin: -0.75rem 0 2rem;
  color: var(--color-muted);
}

.post__cover {
  width: 100%;
  margin-bottom: 2rem;
}

.post__body {
  font-size: 1.0625rem;
  line-height: 1.75;
}

.post__body h2 {
  margin-top: 2.5rem;
}

.post__body figure {
  margin: 2rem 0;
}

.post__body figcaption {
  margin-top: 0.5rem;
  color: var(--color-muted);
  font-size: 0.875rem;
}

.post__body blockquote {
  margin: 2rem 0;
  padding-left: 1.25rem;
  border-left: 3px solid var(--color-accent);
  font-size: 1.25rem;
}


/* ==========================================================================
   13. FOOTER
   ========================================================================== */
.site-footer {
  position: relative;
  z-index: 2;                        /* above the floating icons, so its links stay clickable */
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: center;
  gap: 0.5rem 2rem;
  margin-top: 4rem;
  padding: 1.5rem var(--gutter) 2rem;
}

.site-footer p {
  margin: 0;
}

/* Middle of the footer: the Privacy link and (on carousel pages) the
   pause/play button for the floating icons. */
.site-footer__links {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: 0.25rem 1.5rem;
}

.site-footer__links a {
  display: inline-block;
  padding: 0.25rem 0;                /* comfortable target size */
}

.motion-toggle {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  min-height: 32px;
  padding: 0.25rem 0;
  background: none;
  border: 0;
  color: var(--color-text);
  font: inherit;
  cursor: pointer;
}

.motion-toggle[hidden] {
  display: none;
}

.motion-toggle:hover .motion-toggle__label {
  color: var(--color-accent-hover);
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

/* Pause symbol (two bars), drawn in CSS… */
.motion-toggle__icon {
  width: 10px;
  height: 12px;
  border-left: 3px solid currentColor;
  border-right: 3px solid currentColor;
}

/* …and a play triangle once paused. */
.motion-toggle.is-paused .motion-toggle__icon {
  width: 0;
  height: 0;
  border-top: 6px solid transparent;
  border-bottom: 6px solid transparent;
  border-left: 10px solid currentColor;
  border-right: 0;
}

@media (max-width: 639px) {
  .site-footer {
    flex-direction: column;
    align-items: center;
    text-align: center;
  }
}


/* ==========================================================================
   14. REDUCED MOTION
   Respect visitors who've asked their device to minimise animation.
   (main.js also stops hover videos and the animated logo for them.)
   ========================================================================== */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }

  .gallery__item:hover img {
    transform: none;
  }
}
