/* =========================================================================
 * css/style.css
 * -------------------------------------------------------------------------
 * Every colour, size, radius, shadow and duration below is read from a CSS
 * custom property that js/theme.js publishes out of config/theme.js. Each
 * var() carries a fallback identical to the theme default, so the page is
 * still correct if the theme has not been applied yet (first paint) or if
 * config/theme.js fails to load.
 *
 * To restyle the site, edit config/theme.js — not this file.
 *
 * CONTENTS
 *   1.  Reset & base
 *   2.  Layout shell
 *   3.  Landing overlay
 *   4.  Hero
 *   5.  Section headings
 *   6.  Carousel — frame, track, slides
 *   7.  Media states — skeleton, error, reveal
 *   8.  Video progress bar
 *   9.  Carousel controls — dots, arrows
 *   10. Project cards
 *   11. Tags
 *   12. Project footer buttons
 *   13. Site footer
 *   14. Responsive
 *   15. Motion & contrast preferences
 * ========================================================================= */


/* =========================================================================
 * 1. RESET & BASE
 * ========================================================================= */

*, *::before, *::after { box-sizing: border-box; }

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}

body {
  margin: 0;
  background: var(--colors-background, #fbf9f6);
  color: var(--colors-text, #2c3e50);
  font-family: var(--typography-font-family, 'Segoe UI', Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif);
  font-size: var(--typography-base-size, 16px);
  line-height: var(--typography-base-line-height, 1.65);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow-x: hidden;                     /* no horizontal scroll, ever */
}

h1, h2, h3, p { margin: 0; }

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

button {
  font: inherit;
  color: inherit;
  margin: 0;
  cursor: pointer;
  border: 0;
  background: none;
  -webkit-tap-highlight-color: transparent;
}

a { color: var(--colors-accent-hover, #d68910); }

/* One consistent focus ring, only for keyboard users. */
:focus-visible {
  outline: var(--borders-thickness-bold, 2px) solid var(--colors-focus-ring, rgba(243, 156, 18, 0.6));
  outline-offset: 3px;
  border-radius: var(--borders-radius-small, 10px);
}
:focus:not(:focus-visible) { outline: none; }

.noscript-note {
  max-width: 34rem;
  margin: 4rem auto;
  padding: 0 var(--spacing-page-padding, 6vw);
  text-align: center;
  color: var(--colors-text-muted, #7f8c8d);
}

/* Hide the page while the intro plane is still covering it, so nothing
 * flickers into place behind a translucent scrim. */
.page { position: relative; }


/* =========================================================================
 * 2. LAYOUT SHELL
 * ========================================================================= */

.shell {
  width: 100%;
  max-width: var(--spacing-max-content-width, 1200px);
  margin: 0 auto;
  padding-left: var(--spacing-page-padding, 6vw);
  padding-right: var(--spacing-page-padding, 6vw);
}

.hero {
  padding-top: var(--spacing-page-padding-top, 3rem);
  padding-bottom: var(--spacing-section-spacing, 5rem);
}

.projects { padding-bottom: var(--spacing-section-spacing, 5rem); }


/* =========================================================================
 * 3. LANDING OVERLAY
 * -------------------------------------------------------------------------
 * The scrim, the copy and the button live in one element, so the pop-out
 * animation moves all of it together as a single plane.
 * ========================================================================= */

.landing {
  background: var(--colors-overlay, rgba(30, 46, 61, 0.62));
  -webkit-backdrop-filter: blur(var(--landing-blur, 2px));
  backdrop-filter: blur(var(--landing-blur, 2px));
  color: var(--colors-overlay-text, #ffffff);
  z-index: var(--layers-overlay, 60);
  padding: var(--spacing-page-padding, 6vw);
  /* Transform is applied inline by js/landing.js so the duration and the
   * distance both come from the theme. */
  transition: transform var(--landing-animation-duration-ms, 650ms)
              var(--motion-easing-out, cubic-bezier(0.16, 1, 0.30, 1));
}

.landing.is-leaving { transform: translate3d(0, calc(-1 * var(--landing-lift-distance, 100%)), 0); }

.landing__inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.85rem;
  max-width: 34rem;
}

.landing__eyebrow {
  font-size: var(--typography-eyebrow-size, 0.75rem);
  font-weight: var(--typography-weight-semibold, 600);
  letter-spacing: var(--typography-wide-tracking, 0.15em);
  text-transform: uppercase;
  color: var(--colors-accent, #f39c12);
}

.landing__title {
  font-size: var(--typography-hero-title-size, clamp(2.2rem, 7vw, 4rem));
  font-weight: var(--typography-weight-bold, 700);
  letter-spacing: var(--typography-tight-tracking, -0.02em);
  line-height: 1.08;
  /* A soft shadow rather than a heavier scrim: it buys contrast over moving
   * video without making the carousel underneath any less visible. */
  text-shadow: 0 2px 18px rgba(16, 24, 32, 0.45);
}

.landing__subtitle {
  font-size: var(--typography-hero-subtitle-size, clamp(1rem, 2.4vw, 1.35rem));
  color: var(--colors-overlay-text-muted, rgba(255, 255, 255, 0.88));
  text-shadow: 0 1px 12px rgba(16, 24, 32, 0.42);
}

.landing__arrow {
  margin-top: 1.4rem;
  width: 58px;
  height: 58px;
  display: grid;
  place-items: center;
  border-radius: var(--borders-radius-pill, 999px);
  color: var(--colors-accent-contrast, #ffffff);
  background: var(--colors-accent, #f39c12);
  box-shadow: var(--shadows-button, 0 3px 12px rgba(243, 156, 18, 0.32));
  position: relative;
  z-index: var(--layers-overlay-controls, 70);
  transition: transform var(--motion-duration-fast-ms, 170ms) var(--motion-easing, ease),
              background-color var(--motion-duration-fast-ms, 170ms) var(--motion-easing, ease);
  animation: landing-arrow-bob 1.9s var(--motion-easing, ease) infinite;
}
.landing__arrow svg { width: 26px; height: 26px; }
.landing__arrow:hover { background: var(--colors-accent-hover, #d68910); }
.landing__arrow:active { transform: scale(0.94); }

/* Once the pop-out starts the button is disabled; stop drawing attention. */
.landing__arrow[disabled] {
  cursor: default;
  animation: none;
  opacity: 0.75;
}

@keyframes landing-arrow-bob {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(5px); }
}


/* =========================================================================
 * 4. HERO
 * ========================================================================= */

.hero__head {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  margin-bottom: clamp(1rem, 2.5vw, 1.75rem);
}

.hero__eyebrow {
  font-size: var(--typography-eyebrow-size, 0.75rem);
  font-weight: var(--typography-weight-semibold, 600);
  letter-spacing: var(--typography-wide-tracking, 0.15em);
  text-transform: uppercase;
  color: var(--colors-accent-hover, #d68910);
}

.hero__name {
  font-size: var(--typography-section-title-size, clamp(1.7rem, 4vw, 2.5rem));
  font-weight: var(--typography-weight-bold, 700);
  letter-spacing: var(--typography-tight-tracking, -0.02em);
  color: var(--colors-heading, #22303d);
}

/* The summary carousel is not edge-to-edge: it sits inside the content width
 * and is additionally capped by the theme.
 *
 * The second cap is the important one. A carousel is only allowed to play
 * when it is completely inside the viewport, so a hero even a few pixels
 * taller than the screen would sit silent until the visitor scrolled. Rather
 * than limit the height — which would leave the media pillarboxed inside a
 * too-wide box — the height budget is converted into a WIDTH cap using the
 * aspect ratio, so the box keeps its shape and simply gets smaller. Both the
 * ratio and the reserve come from config/theme.js. */
.hero__carousel {
  --hero-height-budget: calc(100vh - var(--carousel-summary-height-reserve, 11rem));

  width: 100%;
  max-width: min(
    var(--carousel-summary-max-width, 1080px),
    calc(var(--hero-height-budget) * var(--carousel-summary-aspect-ratio-scale, 1.7778))
  );
  margin: 0 auto;
}

/* svh excludes the mobile browser's collapsing toolbars, so the hero still
 * fits when they are showing. */
@supports (height: 100svh) {
  .hero__carousel {
    --hero-height-budget: calc(100svh - var(--carousel-summary-height-reserve, 11rem));
  }
}


/* =========================================================================
 * 5. SECTION HEADINGS
 * ========================================================================= */

.projects__head {
  max-width: 46rem;
  margin-bottom: clamp(1.5rem, 4vw, 2.75rem);
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.projects__title {
  font-size: var(--typography-section-title-size, clamp(1.7rem, 4vw, 2.5rem));
  font-weight: var(--typography-weight-bold, 700);
  letter-spacing: var(--typography-tight-tracking, -0.02em);
  color: var(--colors-heading, #22303d);
}

.projects__intro {
  font-size: var(--typography-body-size, 1rem);
  color: var(--colors-text-muted, #7f8c8d);
}


/* =========================================================================
 * 6. CAROUSEL — FRAME, TRACK, SLIDES
 * ========================================================================= */

.carousel { position: relative; }

/* The aspect box. Media is contained inside it and never stretched, so any
 * leftover space is transparent letterboxing / pillarboxing that shows the
 * page or card colour through. */
.carousel__viewport {
  position: relative;
  overflow: hidden;
  border-radius: var(--carousel-radius, 18px);
  background: var(--colors-media-backdrop, transparent);
  aspect-ratio: var(--carousel-aspect-ratio, 16 / 9);
  /* Vertical panning still works; horizontal is ours to interpret as swipe. */
  touch-action: pan-y;
}

/* Fallback for browsers without aspect-ratio (Safari below 15): the padding
 * trick, using the percentage twin that js/theme.js derives from each ratio. */
@supports not (aspect-ratio: 16 / 9) {
  .carousel--summary .carousel__viewport {
    height: 0;
    padding-bottom: var(--carousel-summary-aspect-ratio-pct, 56.25%);
  }
  .carousel--project .carousel__viewport {
    height: 0;
    padding-bottom: var(--carousel-project-aspect-ratio-pct, 56.25%);
  }
}

.carousel--summary .carousel__viewport {
  aspect-ratio: var(--carousel-summary-aspect-ratio, 16 / 9);
  box-shadow: var(--shadows-media, 0 8px 24px rgba(44, 62, 80, 0.1));
}
.carousel--project .carousel__viewport {
  aspect-ratio: var(--carousel-project-aspect-ratio, 16 / 9);
}

.carousel__track {
  position: absolute;
  inset: 0;
  display: flex;
  will-change: transform;
  transition: transform var(--playback-slide-transition-ms, 480ms)
              var(--motion-easing, cubic-bezier(0.22, 0.61, 0.36, 1));
}
/* Used while restoring position (e.g. deep link) so nothing animates from
 * slide 1 across to slide 4 on arrival. */
.carousel__track.is-instant { transition: none; }

.carousel__slide {
  position: relative;
  flex: 0 0 100%;
  width: 100%;
  height: 100%;
  overflow: hidden;
}

/* Media itself: `contain` is what guarantees nothing is ever stretched. */
.carousel__media {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  opacity: 0;
  transition: opacity var(--assets-fade-duration-ms, 420ms) ease;
  background: transparent;
}
.carousel__slide.is-ready .carousel__media { opacity: 1; }

/* Slides that are not on screen must not capture pointer events, or an
 * off-screen progress bar could swallow a swipe. */
.carousel__slide[aria-hidden="true"] { pointer-events: none; }


/* =========================================================================
 * 7. MEDIA STATES — SKELETON, ERROR, REVEAL
 * ========================================================================= */

.carousel__skeleton {
  position: absolute;
  inset: 0;
  background: var(--colors-skeleton-base, #f1ece2);
  overflow: hidden;
  transition: opacity var(--assets-fade-duration-ms, 420ms) ease;
}
.carousel__skeleton::after {
  content: '';
  position: absolute;
  inset: 0;
  transform: translateX(-100%);
  background: linear-gradient(
    90deg,
    transparent 0%,
    var(--colors-skeleton-sheen, rgba(255, 255, 255, 0.85)) 50%,
    transparent 100%
  );
  animation: skeleton-sweep 1.5s ease-in-out infinite;
}
@keyframes skeleton-sweep {
  100% { transform: translateX(100%); }
}

.carousel__slide.is-ready .carousel__skeleton,
.carousel__slide.is-failed .carousel__skeleton {
  opacity: 0;
  pointer-events: none;
}

/* Fallback tile for an asset that could not be fetched. Keeps the slide the
 * right size so the carousel geometry and dot count stay intact. */
.carousel__error {
  position: absolute;
  inset: 0;
  display: none;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.55rem;
  padding: 1.25rem;
  text-align: center;
  background: var(--colors-background-alt, #f6f1e8);
  color: var(--colors-text-muted, #7f8c8d);
  font-size: var(--typography-caption-size, 0.8rem);
}
.carousel__slide.is-failed .carousel__error { display: flex; }
.carousel__error svg {
  width: 28px;
  height: 28px;
  color: var(--colors-accent, #f39c12);
}
.carousel__error-label {
  max-width: 24rem;
  color: var(--colors-text, #2c3e50);
  font-weight: var(--typography-weight-medium, 500);
}


/* =========================================================================
 * 8. VIDEO PROGRESS BAR
 * -------------------------------------------------------------------------
 * Two nested elements:
 *   .carousel__scrub  the invisible hit area. Tall enough to grab reliably
 *                     (taller still on coarse pointers) and it claims every
 *                     pointer event inside itself, so scrubbing can never be
 *                     read as a carousel swipe.
 *   .carousel__bar    the compact, translucent bar you actually see.
 * 0% and 100% map to the video's configured startTime and endTime, not to
 * the whole file — js/video.js does that mapping.
 * ========================================================================= */

.carousel__scrub {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: var(--carousel-progress-hit-area, 30px);
  padding: var(--carousel-progress-inset, 8px) var(--carousel-progress-inset, 8px) 0;
  z-index: var(--layers-media-controls, 20);
  display: flex;
  align-items: flex-start;
  background: transparent;
  cursor: pointer;
  /* The browser must not steal the drag for scrolling — this element owns it. */
  touch-action: none;
  opacity: 0;
  transition: opacity var(--motion-duration-base-ms, 300ms) ease;
}
@media (hover: none) {
  .carousel__scrub { height: var(--carousel-progress-hit-area-touch, 48px); }
}

/* Visible while the video is the active slide; the hit area stays live a
 * little wider than the eye needs. */
.carousel__slide.is-active .carousel__scrub { opacity: 1; }
.carousel__scrub[hidden] { display: none; }

.carousel__bar {
  position: relative;
  flex: 1 1 auto;
  height: var(--carousel-progress-height, 5px);
  border-radius: var(--borders-radius-pill, 999px);
  background: var(--colors-progress-track, rgba(255, 255, 255, 0.3));
  /* Keeps the translucent bar readable over a bright frame. */
  box-shadow: 0 1px 3px rgba(16, 24, 32, 0.28);
  transition: height var(--motion-duration-fast-ms, 170ms) ease;
  overflow: visible;
}
.carousel__scrub:hover .carousel__bar,
.carousel__scrub.is-dragging .carousel__bar {
  height: var(--carousel-progress-height-hot, 9px);
}

.carousel__bar-fill {
  position: absolute;
  inset: 0 auto 0 0;
  width: 0%;
  border-radius: inherit;
  background: var(--colors-progress-fill, #f39c12);
}

/* Grab handle: only worth showing once the pointer is on the bar. */
.carousel__bar-thumb {
  position: absolute;
  top: 50%;
  left: 0%;
  width: 13px;
  height: 13px;
  margin-left: -6.5px;
  border-radius: var(--borders-radius-pill, 999px);
  background: var(--colors-progress-thumb, #ffffff);
  box-shadow: var(--shadows-control, 0 2px 10px rgba(28, 36, 44, 0.2));
  transform: translateY(-50%) scale(0);
  transition: transform var(--motion-duration-fast-ms, 170ms) ease;
}
.carousel__scrub:hover .carousel__bar-thumb,
.carousel__scrub.is-dragging .carousel__bar-thumb {
  transform: translateY(-50%) scale(1);
}


/* =========================================================================
 * 9. CAROUSEL CONTROLS — DOTS, ARROWS
 * ========================================================================= */

.carousel__dots {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--carousel-dot-gap, 9px);
  margin-top: var(--carousel-dot-row-spacing, 0.95rem);
}
.carousel__dots[hidden] { display: none; }

.carousel__dot {
  width: var(--carousel-dot-size, 9px);
  height: var(--carousel-dot-size, 9px);
  padding: 0;
  border-radius: var(--borders-radius-pill, 999px);
  background: var(--colors-dot-idle, rgba(44, 62, 80, 0.22));
  transition: width var(--motion-duration-base-ms, 300ms) var(--motion-easing, ease),
              background-color var(--motion-duration-base-ms, 300ms) ease;
}
.carousel__dot:hover { background: var(--colors-accent-hover, #d68910); }
.carousel__dot[aria-current="true"] {
  width: var(--carousel-dot-active-width, 30px);
  background: var(--colors-accent, #f39c12);
}

/* Comfortable tap target without changing the dot's visual size. */
.carousel__dot::before {
  content: '';
  position: absolute;
  inset: -11px;
}
.carousel__dot { position: relative; }

.carousel__arrow {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: var(--carousel-arrow-size, 40px);
  height: var(--carousel-arrow-size, 40px);
  display: grid;
  place-items: center;
  border-radius: var(--borders-radius-pill, 999px);
  color: var(--colors-accent-contrast, #ffffff);
  background: var(--colors-scrim, rgba(28, 36, 44, 0.34));
  box-shadow: var(--shadows-control, 0 2px 10px rgba(28, 36, 44, 0.2));
  z-index: var(--layers-media-controls, 20);
  opacity: 0;
  transition: opacity var(--motion-duration-base-ms, 300ms) ease,
              background-color var(--motion-duration-fast-ms, 170ms) ease;
}
.carousel__arrow svg { width: 20px; height: 20px; }
.carousel__arrow--prev { left: var(--carousel-arrow-inset, 10px); }
.carousel__arrow--next { right: var(--carousel-arrow-inset, 10px); }
.carousel__arrow:hover { background: var(--colors-accent, #f39c12); }

.carousel:hover .carousel__arrow,
.carousel__arrow:focus-visible { opacity: 1; }

/* Touch devices get no hover, so keep the arrows quietly present. */
@media (hover: none) {
  .carousel__arrow { opacity: 0.85; }
}


/* =========================================================================
 * 10. PROJECT CARDS
 * -------------------------------------------------------------------------
 * The "floating 3D card" is pure styling: a lighter surface than the page,
 * a hairline border, a two-part shadow (contact + ambient) and a small lift
 * on hover. No transforms on the media, so video stays crisp.
 * ========================================================================= */

.projects__list {
  display: flex;
  flex-direction: column;
  gap: var(--spacing-project-spacing, 4rem);
}

.project {
  position: relative;
  /* This is what keeps the footer's slide-up animation from ever showing
   * outside the card. */
  overflow: hidden;
  padding: var(--spacing-card-padding, 2.1rem);
  border: var(--borders-thickness, 1px) solid var(--colors-border, #f2ede4);
  border-radius: var(--borders-radius-large, 22px);
  background: var(--colors-card, #ffffff);
  box-shadow: var(--shadows-card, 0 1px 2px rgba(44, 62, 80, 0.04), 0 10px 30px rgba(44, 62, 80, 0.07));
  transition: transform var(--motion-duration-base-ms, 300ms) var(--motion-easing, ease),
              box-shadow var(--motion-duration-base-ms, 300ms) var(--motion-easing, ease),
              border-color var(--motion-duration-base-ms, 300ms) ease;
}

.project:hover,
.project:focus-within {
  transform: translateY(var(--motion-card-hover-lift, -4px));
  box-shadow: var(--shadows-card-hover, 0 2px 6px rgba(44, 62, 80, 0.06), 0 22px 50px rgba(44, 62, 80, 0.13));
  border-color: var(--colors-border-strong, #e6ddcd);
}

.project__grid {
  display: grid;
  grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
  gap: var(--spacing-column-gap, 2.75rem);
  align-items: center;
}

/* Alternating layout. The DOM order is always media-then-text so the
 * reading order stays sensible; only the visual columns swap. */
.project--media-left  .project__media { grid-column: 1; }
.project--media-left  .project__body  { grid-column: 2; }
.project--media-right .project__media { grid-column: 2; grid-row: 1; }
.project--media-right .project__body  { grid-column: 1; grid-row: 1; }

.project__media { min-width: 0; }

.project__body {
  min-width: 0;
  /* The footer is absolutely positioned against this box, so its slide-up
   * is measured from the bottom of the text column. */
  display: flex;
  flex-direction: column;
  gap: var(--spacing-stack-gap, 1.1rem);
}

.project__title {
  font-size: var(--typography-project-title-size, clamp(1.4rem, 3.2vw, 2.05rem));
  font-weight: var(--typography-weight-bold, 700);
  letter-spacing: var(--typography-tight-tracking, -0.02em);
  color: var(--colors-heading, #22303d);
  line-height: 1.18;
}

.project__description {
  display: flex;
  flex-direction: column;
  gap: 0.7rem;
  font-size: var(--typography-body-size, 1rem);
  color: var(--colors-text, #2c3e50);
}
.project__description strong { font-weight: var(--typography-weight-semibold, 600); }
.project__description a { text-decoration-thickness: 1px; text-underline-offset: 2px; }


/* =========================================================================
 * 11. TAGS
 * ========================================================================= */

.project__tags {
  display: flex;
  flex-wrap: wrap;                        /* wraps to as many rows as needed */
  gap: 0.45rem;
  margin-top: 0.15rem;
  padding: 0;
  list-style: none;
}
.project__tags[hidden] { display: none; }

.project__tag {
  padding: 0.28rem 0.7rem;
  border: var(--borders-thickness, 1px) solid var(--colors-tag-border, rgba(243, 156, 18, 0.34));
  border-radius: var(--borders-radius-pill, 999px);
  background: var(--colors-tag-background, rgba(243, 156, 18, 0.12));
  color: var(--colors-tag-text, #8a5a06);
  font-size: var(--typography-tag-size, 0.8rem);
  font-weight: var(--typography-weight-medium, 500);
  line-height: 1.5;
  white-space: nowrap;
}


/* =========================================================================
 * 12. PROJECT FOOTER BUTTONS
 * -------------------------------------------------------------------------
 * Collapsed to zero height, and the row inside is translated down out of
 * sight. Hovering the card expands the wrapper and slides the row up into
 * place, above the tags. The wrapper clips its own contents and the card
 * clips everything again, so nothing can escape the container.
 *
 * `height` is animated to a pixel value measured by js/projects.js rather
 * than to a guessed max-height, which keeps the easing honest at any
 * number of buttons and any wrap count.
 * ========================================================================= */

.project__footer {
  overflow: hidden;
  height: 0;
  transition: height var(--motion-duration-base-ms, 300ms) var(--motion-easing-out, ease);
}

.project__footer-row {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;                /* centred, any number of buttons */
  gap: 0.55rem;
  padding-bottom: 0.7rem;
  transform: translateY(100%);
  opacity: 0;
  transition: transform var(--motion-duration-base-ms, 300ms) var(--motion-easing-out, ease),
              opacity var(--motion-duration-base-ms, 300ms) ease;
}

.project.is-footer-open .project__footer-row {
  transform: translateY(0);
  opacity: 1;
}

.project__button {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.46rem 0.95rem;
  border-radius: var(--borders-radius-pill, 999px);
  background: var(--colors-accent, #f39c12);
  color: var(--colors-accent-contrast, #ffffff);
  font-size: var(--typography-button-size, 0.88rem);
  font-weight: var(--typography-weight-semibold, 600);
  text-decoration: none;
  box-shadow: var(--shadows-button, 0 3px 12px rgba(243, 156, 18, 0.32));
  transition: background-color var(--motion-duration-fast-ms, 170ms) ease,
              transform var(--motion-duration-fast-ms, 170ms) ease;
}
.project__button:hover {
  background: var(--colors-accent-hover, #d68910);
  transform: translateY(-2px);
}
.project__button:active { transform: translateY(0); }

/* The external-link arrow — only ever on these buttons. */
.project__button-icon {
  font-size: 0.95em;
  line-height: 1;
  transition: transform var(--motion-duration-fast-ms, 170ms) ease;
}
.project__button:hover .project__button-icon { transform: translate(2px, -2px); }


/* =========================================================================
 * 13. SITE FOOTER
 * ========================================================================= */

.site-footer {
  border-top: var(--borders-thickness, 1px) solid var(--colors-border, #f2ede4);
  background: var(--colors-background-alt, #f6f1e8);
  padding-top: 1.5rem;
  padding-bottom: 1.5rem;
}

.site-footer__inner {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  align-items: center;
  justify-content: space-between;
}

.site-footer__note {
  font-size: var(--typography-caption-size, 0.8rem);
  color: var(--colors-text-muted, #7f8c8d);
}

.site-footer__top {
  padding: 0.4rem 0.9rem;
  border-radius: var(--borders-radius-pill, 999px);
  border: var(--borders-thickness, 1px) solid var(--colors-tag-border, rgba(243, 156, 18, 0.34));
  background: var(--colors-accent-soft, rgba(243, 156, 18, 0.14));
  color: var(--colors-tag-text, #8a5a06);
  font-size: var(--typography-button-size, 0.88rem);
  font-weight: var(--typography-weight-medium, 500);
  transition: background-color var(--motion-duration-fast-ms, 170ms) ease;
}
.site-footer__top:hover {
  background: var(--colors-accent, #f39c12);
  color: var(--colors-accent-contrast, #fff);
}


/* =========================================================================
 * 14. RESPONSIVE
 * ========================================================================= */

/* Tablet: keep two columns but tighten the gap and stop centring the text
 * against a very tall media box. */
@media (max-width: 1024px) {
  .project__grid { gap: clamp(1.1rem, 2.4vw, 1.8rem); align-items: start; }
}

/* Mobile: stack media above text for every project, whatever the
 * alternation would have been. */
@media (max-width: 760px) {
  .project__grid {
    grid-template-columns: minmax(0, 1fr);
    gap: 1.25rem;
  }
  .project--media-left  .project__media,
  .project--media-right .project__media { grid-column: 1; grid-row: 1; }
  .project--media-left  .project__body,
  .project--media-right .project__body  { grid-column: 1; grid-row: 2; }

  .project__tags { gap: 0.4rem; }
  .landing__arrow { width: 52px; height: 52px; }
  .landing__arrow svg { width: 23px; height: 23px; }
}

/* No hover to depend on: the footer buttons are always visible, so they can
 * never become unreachable on a phone. js/projects.js sets the open state
 * for these devices too, this just removes the animation's starting point. */
@media (hover: none) {
  .project__footer-row { transform: none; opacity: 1; }
}


/* =========================================================================
 * 15. MOTION & CONTRAST PREFERENCES
 * ========================================================================= */

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
  /* Keep the reveal, drop the sweep. */
  .carousel__skeleton::after { animation: none; }
  .landing__arrow { animation: none; }
}

@media (prefers-contrast: more) {
  .project { border-color: var(--colors-text-muted, #7f8c8d); }
  .carousel__bar { background: rgba(255, 255, 255, 0.6); }
  .landing { background: rgba(24, 38, 51, 0.86); }
}
