/* =========================================================
   Design tokens
   ========================================================= */
:root {
  --color-button: #3d6487;
  --color-button-hover: #345670;
  --color-header: #587b99;
  --color-bg: #F9F5EB;
  --color-elevated: #E8E5DD;
  --color-accent: #B4A36B;
  --color-text: #130E01;

  --font-mono: "Geist Mono", "Geist Mono Fallback", ui-monospace, SFMono-Regular,
    "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;

  --nav-height: 72px;
  --max-width: 1200px;
  --radius-lg: 20px;
  --radius-md: 12px;
  --transition-panel: transform 0.45s cubic-bezier(0.4, 0, 0.2, 1);
}

/* =========================================================
   Reset
   ========================================================= */
* {
  box-sizing: border-box;
}

html {
  scroll-behavior: smooth;
}

body {
  margin: 0;
  background: var(--color-bg);
  color: var(--color-text);
  font-family: var(--font-mono);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3 {
  font-family: var(--font-mono);
  color: var(--color-header);
  margin: 0 0 0.5em;
}

a {
  color: inherit;
}

button {
  font-family: inherit;
}

/* =========================================================
   Top navigation
   ========================================================= */
.navbar {
  position: sticky;
  top: 0;
  z-index: 120;
  height: var(--nav-height);
  background: var(--color-bg);
  border-bottom: 1px solid rgba(19, 14, 1, 0.08);
}

.nav-inner {
  max-width: var(--max-width);
  height: 100%;
  margin: 0 auto;
  padding: 0 32px;
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.logo {
  font-weight: 700;
  font-size: 1.25rem;
  letter-spacing: 0.02em;
  text-decoration: none;
  color: var(--color-header);
}

.nav-links {
  display: flex;
  align-items: center;
  gap: 24px;
}

.nav-link {
  background: none;
  border: none;
  cursor: pointer;
  font-size: 0.95rem;
  font-weight: 500;
  color: var(--color-text);
  padding: 8px 4px;
  position: relative;
}

.nav-link::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 2px;
  background: var(--color-accent);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.25s ease;
}

.nav-link:hover::after {
  transform: scaleX(1);
}

/* =========================================================
   Carousel
   ========================================================= */
.carousel-section {
  position: relative;
  padding: 56px 0 40px;
  /* Distinct blue band that separates the carousel from the beige page.
     Also the color the shader feathers into at the top/bottom edges. */
  background: var(--color-button);
  isolation: isolate; /* own stacking context so the canvas can't escape behind body */
  overflow: hidden;
}

/* Animated WebGL backdrop */
.shader-backdrop {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  z-index: -2;
}

/* Blue veil over the shader. 0.25 alpha through the middle = shader ~75%
   visible; ramps to solid blue at the top/bottom so the shader dissolves
   into the band edges instead of hard-cutting against the beige page. */
.carousel-section::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  /* Flat veil, no edge ramps.
     The shader no longer dissolves into the cream page at the top/bottom —
     the band keeps a clean edge and stays at a constant 75% shader
     visibility all the way across. The fade now lives on the carousel
     track instead (see .carousel-track's mask below), so it's the cards
     that dissolve into the shader rather than the shader into the page.
     Lower this alpha for more shader, raise it for less. */
  background: rgba(61, 100, 135, 0.25);
}

.carousel-viewport {
  position: relative;
  z-index: 1;
  max-width: var(--max-width);
  margin: 0 auto;
}

.carousel-track {
  /* position:relative makes the track the offsetParent for slides, so
     slide.offsetLeft is directly comparable to track.scrollLeft. */
  position: relative;
  display: flex;
  gap: 24px;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  /* NOTE: deliberately no `scroll-behavior: smooth` here. The infinite-loop
     teleport uses scrollTo({behavior:"auto"}) and a CSS smooth value would
     override it, animating the jump and making the seam visible. Smooth
     scrolling is opted into per-call from main.js instead. */
  padding: 8px 12% 24px;
  -ms-overflow-style: none;
  scrollbar-width: none;

  /* Edge fade on the CARDS, not the shader.
     Cards dissolve into the shader as they scroll toward either edge
     instead of being hard-clipped at the viewport boundary. Eased with a
     smoothstep curve (t²(3-2t)) so there's no banding in the ramp.
     The arrows are siblings in .carousel-viewport, so they stay crisp.
     Widen the 15% stops for a longer dissolve. */
  -webkit-mask-image: linear-gradient(
    to right,
    rgba(0,0,0,0.000) 0.00%,
    rgba(0,0,0,0.074) 2.50%,
    rgba(0,0,0,0.259) 5.00%,
    rgba(0,0,0,0.500) 7.50%,
    rgba(0,0,0,0.741) 10.00%,
    rgba(0,0,0,0.926) 12.50%,
    rgba(0,0,0,1.000) 15.00%,
    rgba(0,0,0,1.000) 85.00%,
    rgba(0,0,0,0.926) 87.50%,
    rgba(0,0,0,0.741) 90.00%,
    rgba(0,0,0,0.500) 92.50%,
    rgba(0,0,0,0.259) 95.00%,
    rgba(0,0,0,0.074) 97.50%,
    rgba(0,0,0,0.000) 100.00%
  );
  mask-image: linear-gradient(
    to right,
    rgba(0,0,0,0.000) 0.00%,
    rgba(0,0,0,0.074) 2.50%,
    rgba(0,0,0,0.259) 5.00%,
    rgba(0,0,0,0.500) 7.50%,
    rgba(0,0,0,0.741) 10.00%,
    rgba(0,0,0,0.926) 12.50%,
    rgba(0,0,0,1.000) 15.00%,
    rgba(0,0,0,1.000) 85.00%,
    rgba(0,0,0,0.926) 87.50%,
    rgba(0,0,0,0.741) 90.00%,
    rgba(0,0,0,0.500) 92.50%,
    rgba(0,0,0,0.259) 95.00%,
    rgba(0,0,0,0.074) 97.50%,
    rgba(0,0,0,0.000) 100.00%
  );
}

.carousel-track::-webkit-scrollbar {
  display: none;
}

.carousel-slide {
  position: relative;
  flex: 0 0 76%;
  aspect-ratio: 16 / 9;
  scroll-snap-align: center;
  border-radius: var(--radius-lg);
  overflow: hidden;
  background: var(--color-header);
  cursor: pointer;
  opacity: 0.55;
  transform: scale(0.92);
  transition: opacity 0.4s ease, transform 0.4s ease, box-shadow 0.4s ease;
  box-shadow: 0 8px 28px rgba(19, 14, 1, 0.14);
}

.carousel-slide.is-active {
  opacity: 1;
  transform: scale(1);
  box-shadow: 0 16px 44px rgba(19, 14, 1, 0.26);
}

/* Applied by main.js for the single frame in which the infinite-loop
   teleport swaps .is-active onto the equivalent cloned slide. Without this
   the incoming slide animates up from the inactive state, producing a
   visible "pop" every time the carousel wraps. Higher specificity (0,3,0)
   than .carousel-slide (0,1,0), so no !important needed. */
.carousel-track.is-teleporting .carousel-slide,
.carousel-track.is-teleporting .play-btn {
  transition: none !important;
}

/* Mandatory snapping re-runs its settle animation when scrollLeft is set
   programmatically, which shows up as a stutter right at the wrap point.
   Suspending snap for the duration of the teleport lets the jump land
   exactly where we put it; snap is restored on the next frame. */
.carousel-track.is-teleporting {
  scroll-snap-type: none;
}

.carousel-slide__media {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

.placeholder-media {
  background: linear-gradient(135deg, var(--color-header), var(--color-accent));
}

.placeholder-media--a { background: linear-gradient(135deg, var(--color-header), var(--color-button)); }
.placeholder-media--b { background: linear-gradient(135deg, var(--color-accent), var(--color-header)); }
.placeholder-media--c { background: linear-gradient(135deg, var(--color-button), var(--color-accent)); }

.carousel-slide__scrim {
  position: absolute;
  inset: 0;
  background: linear-gradient(0deg, rgba(19, 14, 1, 0.55) 0%, rgba(19, 14, 1, 0) 45%);
}

.carousel-slide__title {
  position: absolute;
  left: 28px;
  bottom: 24px;
  right: 96px;
  color: #F9F5EB;
  font-size: clamp(1.25rem, 2.4vw, 2rem);
  font-weight: 700;
  margin: 0;
  z-index: 2;
}

.play-btn {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 76px;
  height: 76px;
  border-radius: 50%;
  border: none;
  background: #F9F5EB;
  color: var(--color-text);
  font-weight: 600;
  font-size: 0.95rem;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 8px 24px rgba(19, 14, 1, 0.25);
  z-index: 2;

  /* Only the centred slide shows a Play button. The peeking slides are
     half off-screen, so their centred button lands right on top of the
     prev/next arrows — which read as two overlapping buttons. */
  opacity: 0;
  pointer-events: none;

  /* Fade OUT fast and with no delay. A CSS transition is chosen from the
     AFTER-change style, so a slide losing .is-active picks up this rule and
     is fully gone in 120ms. */
  transition: opacity 0.12s ease, transform 0.2s ease;
}


.carousel-slide.is-active .play-btn {
  opacity: 1;
  pointer-events: auto;

  /* Fade IN only after the outgoing button has finished disappearing
     (0.12s delay == the fade-out duration above). Without this sequencing
     the two crossfade, putting two semi-transparent Play buttons on screen
     at once — one of which lands underneath the prev/next arrow. */
  transition: opacity 0.22s ease 0.12s, transform 0.2s ease;
}

.carousel-slide.is-active .play-btn:hover {
  transform: translate(-50%, -50%) scale(1.06);
}

/* Force the play button to hide while the track is actively scrolling */
.carousel-track.is-scrolling .play-btn {
  opacity: 0 !important;
  pointer-events: none;
  transition: opacity 0.12s ease;
}

.carousel-arrow {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 48px;
  height: 48px;
  border-radius: 50%;
  border: none;
  background: #F9F5EB;
  color: var(--color-text);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  z-index: 5;
  box-shadow: 0 4px 16px rgba(19, 14, 1, 0.18);
  transition: background 0.2s ease;
}

.carousel-arrow:hover {
  background: var(--color-elevated);
}

.carousel-arrow--prev { left: 4px; }
.carousel-arrow--next { right: 4px; }

.carousel-dots {
  position: relative;
  z-index: 1;
  display: flex;
  justify-content: center;
  gap: 10px;
  margin-top: 8px;
}

.carousel-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  border: none;
  /* light dots — they now sit on the blue band, not the beige page */
  background: rgba(249, 245, 235, 0.4);
  cursor: pointer;
  padding: 0;
  transition: background 0.2s ease, transform 0.2s ease;
}

.carousel-dot.is-active {
  background: var(--color-accent);
  transform: scale(1.25);
}

/* =========================================================
   Caption bar
   ========================================================= */
.caption-bar {
  max-width: var(--max-width);
  margin: 0 auto;
  padding: 14px 32px;
  border-top: 1px solid rgba(19, 14, 1, 0.1);
  border-bottom: 1px solid rgba(19, 14, 1, 0.1);
}

.caption-bar__text {
  margin: 0;
  text-align: center;
  font-size: 0.95rem;
  color: var(--color-text);
  opacity: 0.75;
  transition: opacity 0.2s ease;
}

/* =========================================================
   Resume
   ========================================================= */
.resume-section {
  padding: 48px 32px 96px;
}

.resume-card {
  max-width: var(--max-width);
  margin: 0 auto;
  background: var(--color-elevated);
  border-radius: var(--radius-lg);
  padding: 48px;
  box-shadow: 0 2px 24px rgba(19, 14, 1, 0.06);
}

.resume-card__header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 24px;
  margin-bottom: 32px;
  padding-bottom: 24px;
  border-bottom: 1px solid rgba(19, 14, 1, 0.12);
}

.resume-card__heading {
  min-width: 0; /* let a long name wrap instead of squashing the button */
}

.resume-download {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  gap: 9px;
  padding: 10px 18px;
  border-radius: 999px;
  background: var(--color-button);
  color: var(--color-bg);
  font-size: 0.85rem;
  font-weight: 500;
  text-decoration: none;
  white-space: nowrap;
  transition: background 0.2s ease, transform 0.2s ease;
}

.resume-download:hover {
  background: var(--color-button-hover);
  transform: translateY(-1px);
}

.resume-download:active {
  transform: translateY(0);
}

.resume-download svg {
  flex-shrink: 0;
}

.resume-name {
  font-size: clamp(1.75rem, 3vw, 2.5rem);
}

.resume-tagline {
  margin: 0;
  color: var(--color-text);
  opacity: 0.7;
}

.resume-grid {
  display: grid;
  grid-template-columns: 2fr 1fr;
  gap: 48px;
}

.resume-block {
  margin-bottom: 32px;
}

.resume-block h2 {
  font-size: 1rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--color-button);
  margin-bottom: 16px;
}

.resume-entry {
  margin-bottom: 20px;
}

/* Two-row entry:
     row 1  Organisation (bold)          Dates (italic, right)
     row 2  Role (italic)                Location (italic, right)   */
.resume-entry__row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
}

.resume-entry__row + .resume-entry__row {
  margin-top: 1px;
}

.resume-entry__org {
  font-weight: 700;
}

.resume-entry__role {
  font-style: italic;
  opacity: 0.9;
}

/* Right-hand column: dates and location share the same treatment so they
   line up as a single right-aligned stack. */
.resume-entry__date,
.resume-entry__location {
  flex-shrink: 0;
  font-style: italic;
  text-align: right;
  white-space: nowrap;
  opacity: 0.9;
}

.resume-entry__list {
  margin-top: 8px;
}

.resume-entry__list {
  margin: 0;
  padding-left: 18px;
  font-size: 0.95rem;
}

.resume-entry__list li {
  margin-bottom: 4px;
}

.resume-project-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.project-link {
  display: inline-block;
  text-decoration: none;
  font-weight: 600;
  color: var(--color-button);
  border-bottom: 1px solid transparent;
  transition: border-color 0.2s ease, color 0.2s ease;
}

.project-link:hover {
  border-color: var(--color-button);
  color: var(--color-button-hover);
}

.resume-tag-list,
.resume-contact-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  font-size: 0.9rem;
}

.resume-tag-list li {
  background: var(--color-bg);
  border: 1px solid rgba(19, 14, 1, 0.1);
  border-radius: 999px;
  padding: 5px 14px;
}

.resume-contact-list {
  flex-direction: column;
  gap: 10px;
}

.contact-link {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  /* colour rather than opacity, so the currentColor SVG icons stay crisp
     instead of being composited against the card background */
  color: rgba(19, 14, 1, 0.85);
  text-decoration: none;
  overflow-wrap: anywhere; /* long email shouldn't overflow the narrow column */
  transition: color 0.2s ease;
}

.contact-link:hover {
  color: var(--color-button);
}

.contact-icon {
  flex-shrink: 0;
  width: 16px;
  height: 16px;
}

/* =========================================================
   Side panels (project + about)
   ========================================================= */
.overlay {
  position: fixed;
  inset: 0;
  background: rgba(19, 14, 1, 0.45);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.35s ease;
  z-index: 150;
}

.overlay.is-visible {
  opacity: 1;
  pointer-events: auto;
}

.side-panel {
  position: fixed;
  top: 0;
  right: 0;
  height: 100vh;
  width: 50%;
  background: var(--color-elevated);
  box-shadow: -12px 0 32px rgba(19, 14, 1, 0.2);
  transform: translateX(100%);
  transition: var(--transition-panel);
  z-index: 200;
  overflow-y: auto;
}

.side-panel.is-open {
  transform: translateX(0);
}

/* Close button.
   ------------------------------------------------------------------
   TO REPOSITION, change these two values:
       top    -> distance from the top of the panel
       margin -> the `20px` is the distance from the RIGHT edge
   e.g. `top: 16px` + `margin: 0 16px 0 auto` sits it tighter into
   the corner. Both are plain pixel gaps; no other numbers depend on them.

   `margin-left: auto` on a fixed-width block pushes it flush right,
   which is why this stays aligned no matter how the panel is padded.
   (It previously used `left: 100%` + a translateX magic number, which
   drifted whenever the surrounding layout changed.)
   ------------------------------------------------------------------ */
.side-panel__close {
  position: sticky;
  top: 20px;
  margin: 0 20px 0 auto;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  border: none;
  background: var(--color-bg);
  color: var(--color-text);
  font-size: 1.4rem;
  line-height: 1;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  /* above the media and the coloured title band */
  z-index: 3;
  box-shadow: 0 2px 10px rgba(19, 14, 1, 0.22);
}

.side-panel__close:hover {
  background: var(--color-elevated);
}

.side-panel__content {
  padding: 0 48px 64px;
  margin-top: -40px;
}

/* Media slot: holds either a YouTube demo reel, a still image, or the
   gradient fallback. Full-bleed to the panel edges via negative margins
   that cancel .side-panel__content's horizontal padding. */
.side-panel__media {
  position: relative;
  margin: 0 -48px;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  background-size: cover;
  background-position: center;
}

/* An embedded video fills the slot exactly like a background image would */
.side-panel__media iframe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
  display: block;
}

/* Title bar sits directly under the media as a solid colour band */
.side-panel__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  margin: 0 -48px 28px;
  padding: 20px 48px;
  background: var(--color-header);
}

.side-panel__title {
  font-size: clamp(1.4rem, 2.2vw, 1.85rem);
  margin: 0;
  color: #F9F5EB;
}

/* Cream circle on the blue band — the previous --color-button fill was
   too close to --color-header to read against it. */
.side-panel__github {
  flex-shrink: 0;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background: var(--color-bg);
  color: var(--color-button);
  display: flex;
  align-items: center;
  justify-content: center;
  text-decoration: none;
  transition: background 0.2s ease, color 0.2s ease, transform 0.2s ease;
}

.side-panel__github:hover {
  background: var(--color-accent);
  color: var(--color-bg);
  transform: scale(1.06);
}

/* --- Size dials for the GitHub button ---
   `width`/`height` on .side-panel__github above sets the CIRCLE (44px).
   The rule below sets the MARK inside it. Change this number alone to
   make the logo bigger or smaller without resizing the circle. */
.side-panel__github svg {
  width: 30px;
  height: 30px;
}

/* The About panel has a bare title with no band, so keep it on the
   text colour rather than inheriting the white used on the blue bar. */
.side-panel--about .side-panel__title {
  color: var(--color-header);
}

.side-panel__body {
  font-size: 1rem;
  /* NOTE: do not use `opacity` here. Opacity applies to the whole subtree,
     so it would also fade images, figures and video embeds into the panel
     background. Soften the text via the colour channel instead, which
     leaves media at full strength. */
  color: var(--color-text);
}

/* --- Rendered markdown inside the project panel --- */
.side-panel__body h2 {
  font-size: 1.15rem;
  margin: 28px 0 10px;
}

.side-panel__body h3 {
  font-size: 1rem;
  margin: 22px 0 8px;
  color: var(--color-button);
}

.side-panel__body > :first-child {
  margin-top: 0;
}

.side-panel__body a {
  color: var(--color-button);
  text-decoration: underline;
  text-underline-offset: 2px;
}

.side-panel__body a:hover {
  color: var(--color-button-hover);
}

.side-panel__body p {
  line-height: 1.6;
}

.side-panel__body img {
  max-width: 100%;
  height: auto;
  border-radius: var(--radius-md);
  display: block;
}

.side-panel__body figure {
  margin: 18px 0;
}

.side-panel__body figcaption {
  font-size: 0.85rem;
  /* colour, not opacity — a figcaption sits inside <figure> alongside the
     image, and opacity here is safe, but keeping it consistent avoids the
     same trap if the caption ever gains an icon or thumbnail. */
  color: rgba(19, 14, 1, 0.6);
  margin-top: 6px;
}

/* YouTube embeds: the source markdown hardcoded 640x480, which overflows
   the panel. Force them to fill the column at 16:9 instead. */
.side-panel__body iframe {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 9;
  border: 0;
  border-radius: var(--radius-md);
  margin: 10px 0 22px;
}

/* =========================================================
   Video gallery (project panels)
   Two-up grid of click-to-play tiles. Nothing loads a YouTube
   iframe until the tile is clicked, so a project with six videos
   still opens instantly.
   ========================================================= */
.video-gallery {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 16px;
  margin: 24px 0 8px;
}

.video-tile {
  position: relative;
  display: block;
  width: 100%;
  padding: 0;
  border: none;
  border-radius: var(--radius-md);
  overflow: hidden;
  aspect-ratio: 16 / 9;
  background: var(--color-header);
  cursor: pointer;
  transition: transform 0.2s ease, box-shadow 0.2s ease;
}

.video-tile:hover {
  transform: translateY(-2px);
  box-shadow: 0 10px 24px rgba(19, 14, 1, 0.22);
}

/* hqdefault is 4:3 with letterboxing; `cover` crops the bars away
   and leaves a clean 16:9 frame. */
.video-tile__thumb {
  position: absolute;
  inset: 0;
  background-size: cover;
  background-position: center;
}

.video-tile__play {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 46px;
  height: 46px;
  border-radius: 50%;
  background: var(--color-bg);
  box-shadow: 0 4px 14px rgba(19, 14, 1, 0.3);
  transition: transform 0.2s ease;
}

/* CSS triangle — avoids shipping another icon */
.video-tile__play::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 54%;
  transform: translate(-50%, -50%);
  border-style: solid;
  border-width: 8px 0 8px 13px;
  border-color: transparent transparent transparent var(--color-button);
}

.video-tile:hover .video-tile__play {
  transform: translate(-50%, -50%) scale(1.1);
}

.video-tile__title {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  padding: 22px 12px 10px;
  font-family: var(--font-mono);
  font-size: 0.8rem;
  line-height: 1.3;
  text-align: left;
  color: #F9F5EB;
  background: linear-gradient(0deg, rgba(19, 14, 1, 0.8) 0%, rgba(19, 14, 1, 0) 100%);
}

/* Once clicked, the tile is replaced by this wrapper holding the iframe */
.video-gallery .video-embed {
  position: relative;
  aspect-ratio: 16 / 9;
  border-radius: var(--radius-md);
  overflow: hidden;
  background: #000;
}

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

@media (max-width: 640px) {
  .video-gallery {
    grid-template-columns: 1fr;
  }
}

.side-panel__body code {
  background: var(--color-bg);
  border-radius: 4px;
  padding: 1px 5px;
  font-size: 0.9em;
}

.side-panel--about .side-panel__content {
  margin-top: 24px;
}

/* =========================================================
   Responsive
   ========================================================= */
@media (max-width: 900px) {
  .resume-grid {
    grid-template-columns: 1fr;
  }
}

@media (max-width: 640px) {
  .nav-inner {
    padding: 0 20px;
  }

  .carousel-track {
    padding: 8px 8% 24px;
  }

  .carousel-slide {
    flex: 0 0 88%;
  }

  .resume-section {
    padding: 32px 16px 64px;
  }

  .resume-card {
    padding: 28px 20px;
  }

  /* Stack the download button under the name rather than crushing it */
  .resume-card__header {
    flex-direction: column;
    align-items: flex-start;
    gap: 18px;
  }

  .side-panel {
    width: 100%;
  }

  .side-panel__content {
    padding: 0 24px 48px;
  }

  /* Re-match the full-bleed offsets to the narrower mobile padding */
  .side-panel__media {
    margin: 0 -24px;
  }

  .side-panel__header {
    margin: 0 -24px 22px;
    padding: 16px 24px;
  }
}

/* =========================================================
   GALLERY PAGE
   ========================================================= */
.gallery-page {
  max-width: var(--max-width);
  margin: 0 auto;
  padding: 48px 32px 96px;
}

.gallery-header {
  margin-bottom: 28px;
}

.gallery-title {
  font-size: clamp(1.8rem, 3vw, 2.5rem);
  margin: 0 0 20px;
}

/* ---- Tabs ---- */
.gallery-tabs {
  display: flex;
  gap: 4px;
  border-bottom: 1px solid rgba(19, 14, 1, 0.14);
}

.gallery-tab {
  position: relative;
  background: none;
  border: none;
  cursor: pointer;
  font-family: var(--font-mono);
  font-size: 0.95rem;
  color: rgba(19, 14, 1, 0.55);
  padding: 10px 18px;
  transition: color 0.2s ease;
}

.gallery-tab:hover {
  color: var(--color-text);
}

.gallery-tab.is-active {
  color: var(--color-button);
  font-weight: 600;
}

/* Underline rides on the active tab, overlapping the container border */
.gallery-tab::after {
  content: "";
  position: absolute;
  left: 12px;
  right: 12px;
  bottom: -1px;
  height: 2px;
  background: var(--color-accent);
  transform: scaleX(0);
  transition: transform 0.22s ease;
}

.gallery-tab.is-active::after {
  transform: scaleX(1);
}

.gallery-panel[hidden] {
  display: none;
}

.gallery-empty {
  padding: 56px 24px;
  text-align: center;
  color: rgba(19, 14, 1, 0.5);
  background: var(--color-elevated);
  border-radius: var(--radius-lg);
  font-size: 0.9rem;
}

/* ---- Justified rows ----
   Each <img> is a flex item with a fixed height, so its flex-basis is its
   own aspect-correct width. flex-grow then stretches the row to fill the
   line exactly, and object-fit:cover absorbs the stretch as a tiny crop
   rather than distortion. Net effect: portrait and landscape mix freely
   with no per-image configuration, the way Flickr or Google Photos pack
   a grid. */
.album {
  /* ===== IMAGE SIZE DIAL =====
     Target row height. This is the one number to change.
       larger  -> fewer, bigger images per row
       smaller -> more, smaller images per row
     It is a TARGET, not a fixed size: each image is scaled to this
     height to work out its share of the row, then the row is stretched
     to fill the width exactly. So actual heights land near this value,
     not on it.
     Override per tab with `row_height:` in gallery.html's front matter. */
  --album-row-h: 260px;

  /* How narrow a single image may be squeezed before the row rebalances.
     Raise it if very tall/narrow images end up as slivers. */
  --album-min-w: 180px;

  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 24px;
}

.album-img {
  /* min() keeps the gallery sane on small screens even when a tab sets
     its own --album-row-h inline, which would otherwise beat a media
     query on specificity. 45vw is roughly "no taller than half the
     screen width". */
  height: min(var(--album-row-h), 45vw);
  flex: 1 1 auto;
  min-width: var(--album-min-w);
  max-width: 100%;
  object-fit: cover;
  display: block;
  border-radius: var(--radius-md);
  cursor: zoom-in;
  background: var(--color-elevated);
  transition: transform 0.2s ease, box-shadow 0.2s ease;
}

.album-img:hover,
.album-img:focus-visible {
  transform: translateY(-2px);
  box-shadow: 0 10px 26px rgba(19, 14, 1, 0.24);
  outline: none;
}

/* Soaks up the slack on the final row so a lone trailing image isn't
   stretched across the full width. */
.album::after {
  content: "";
  flex-grow: 999;
  height: 0;
}

/* ---- Lightbox ---- */
.lightbox {
  position: fixed;
  inset: 0;
  z-index: 300;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 40px;
  background: rgba(19, 14, 1, 0.88);
  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;
  border-radius: var(--radius-md);
}

.lightbox__close {
  position: absolute;
  top: 20px;
  right: 20px;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  border: none;
  background: var(--color-bg);
  color: var(--color-text);
  font-size: 1.5rem;
  line-height: 1;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
}

.lightbox__close:hover {
  background: var(--color-accent);
}

/* Marks the current page in the top nav */
.nav-link.is-current {
  color: var(--color-button);
  font-weight: 600;
}

.nav-link.is-current::after {
  transform: scaleX(1);
}

/* The nav mixes <a> and <button>; normalise the anchor */
a.nav-link {
  text-decoration: none;
  display: inline-block;
}

@media (max-width: 640px) {
  .gallery-page {
    padding: 28px 16px 64px;
  }

  /* No .album-img height override needed here - the min(..., 45vw) above
     handles small screens, and unlike a media query it still applies when
     a tab sets --album-row-h inline. */

  .gallery-tab {
    padding: 10px 12px;
    font-size: 0.88rem;
  }

  .lightbox {
    padding: 16px;
  }
}
