:root {
  --cream: #f7f1df;
  --black: #050505;
  --orange: #ff7a1a;
  --pink: #f5a6d6;
  --lime: #c9ff4a;
  --sky: #94d8ff;
  --yellow: #ffe45c;
  --mint: #7ce8c0;
  --border: 4px solid var(--black);
  --radius-lg: 38px;
  --radius-md: 28px;
  --bio-width: 300px;
  --content-clearance: 11.5rem;
}

* { box-sizing: border-box; }

/* No CSS scroll-behavior: smooth here — all animated scrolling is driven by the
   custom rAF tween in main.js, which needs plain instant window.scrollTo() calls
   to step through each frame without a competing native smooth animation. */

body {
  margin: 0;
  font-family: Arial, Helvetica, sans-serif;
  background: var(--cream);
  color: var(--black);
  overflow-x: hidden;
  overflow-wrap: break-word;
}

body::before {
  content: ""; position: fixed; inset: 0; pointer-events: none; opacity: 0.1;
  background-image: radial-gradient(#000 1px, transparent 1px);
  background-size: 10px 10px; z-index: 0;
}

a, button { font: inherit; color: inherit; text-decoration: none; }
button { background: none; border: none; }
a:focus-visible, button:focus-visible { outline: 4px solid var(--orange); outline-offset: 4px; }

/* Header: just "Dave Whelan". A flying clone (see .flying-label) handles the
   actual leap into the sidebar, so the real element just snaps instantly
   between shown/hidden the moment the clone takes over. */
.site-header {
  position: fixed; top: 0; left: 0; right: 0; z-index: 80;
  display: flex; align-items: center;
  padding: 1.3rem 2rem 1.3rem calc(var(--bio-width) + 2rem);
  font-weight: 900; text-transform: uppercase; letter-spacing: 0.2em; font-size: 0.95rem;
  pointer-events: none;
}
.site-header a { pointer-events: auto; }
.site-name { flex-shrink: 0; }
.site-name.is-hidden { opacity: 0; pointer-events: none; }

/* "Digital Media" starts resting on the right of this lane. Leaving the intro
   sends it travelling across to the left, then a flying clone leaps it over
   the bio panel's black border to dock under the portrait (see .sidebar-role)
   and it stays there — it doesn't keep crossing on every panel after that. */
.role-track {
  position: fixed; top: 0; left: var(--bio-width); right: 0; z-index: 80;
  padding: 1.3rem 2rem; display: flex; align-items: center; justify-content: flex-end;
  font-weight: 900; text-transform: uppercase; letter-spacing: 0.2em; font-size: 0.95rem;
  pointer-events: none;
}
.site-role {
  display: inline-block; pointer-events: auto;
  transition: transform 0.6s cubic-bezier(0.65, 0, 0.35, 1);
}

/* Thrown in fresh for each header<->sidebar leap and removed on landing — an
   arced flight (see flyLabel() in main.js) crossing over the bio panel's
   black border, rather than the two ends just fading in and out in place. */
.flying-label {
  position: fixed; top: 0; left: 0; z-index: 95; margin: 0;
  white-space: nowrap; pointer-events: none; font-weight: 900; text-transform: uppercase;
}
.flying-label--name { font-size: 1.15rem; letter-spacing: 0.15em; color: var(--black); }
.flying-label--role { font-size: 0.85rem; letter-spacing: 0.16em; color: var(--orange); }
.flying-label--name-header, .flying-label--role-header { font-size: 0.95rem; letter-spacing: 0.2em; color: var(--black); }

/* Pull-quotes from new-media/interactive-storytelling figures: a fixed strip
   centered over the content area (not the sidebar), sitting right above each
   panel's media. A new quote slides in and settles on every panel change,
   simply replacing the last one — no masking, no clipping, dots stay visible. */
.panel-quote {
  position: fixed; top: 4rem; left: var(--bio-width); right: 0; z-index: 60;
  padding: 0.5rem 3rem 0; display: flex; flex-direction: column; align-items: center;
  text-align: center; pointer-events: none; opacity: 0;
  transition: transform 0.55s cubic-bezier(0.65, 0, 0.35, 1), opacity 0.55s ease;
}
.panel-quote-text {
  margin: 0; font-family: Georgia, "Times New Roman", serif; font-style: italic; font-weight: 700;
  color: var(--orange); font-size: clamp(1.15rem, 1.9vw, 1.85rem); line-height: 1.25;
  text-transform: none; letter-spacing: normal; max-width: 46ch;
  display: -webkit-box; -webkit-line-clamp: 2; line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.panel-quote-author {
  display: block; margin-top: 0.3rem; font-style: normal; font-family: Arial, Helvetica, sans-serif;
  font-size: 0.65rem; font-weight: 900; text-transform: uppercase; letter-spacing: 0.14em;
  color: var(--orange); opacity: 0.75;
}

/* Progress rail */
.progress-rail { position: fixed; top: 0; left: 0; right: 0; height: 4px; background: rgba(5, 5, 5, 0.12); z-index: 90; }
.progress-fill { height: 100%; width: 0%; background: var(--orange); transition: width 0.1s linear; }

/* Bio panel (fixed left rail) */
.bio-panel {
  position: fixed; top: 0; left: 0; width: var(--bio-width); height: 100vh;
  padding: 4.5rem 1.75rem 2rem; display: flex; flex-direction: column; gap: 1.4rem;
  background: var(--cream); border-right: var(--border); z-index: 70;
}
/* Hidden until you scroll past the intro, then the flying clone lands here —
   this just snaps to visible the instant it does, no separate motion of its own. */
.sidebar-name {
  margin: 0; align-self: center; text-align: center;
  font-weight: 900; text-transform: uppercase; letter-spacing: 0.15em; font-size: 1.15rem;
  opacity: 0; transition: opacity 0.15s ease;
}
.sidebar-name.visible { opacity: 1; }

.bio-mark {
  width: 180px; height: 180px; border-radius: var(--radius-lg); flex-shrink: 0;
  background: var(--black); border: var(--border); overflow: hidden;
  display: flex; align-items: center; justify-content: center;
  align-self: center; box-shadow: 0 20px 40px rgba(0, 0, 0, 0.25);
}
.bio-mark img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* "Digital Media" docks here, right under the portrait, once its flying clone
   lands (see .flying-label--role in the header block above). */
.sidebar-role {
  margin: -0.5rem 0 0; align-self: center; text-align: center;
  font-weight: 900; text-transform: uppercase; letter-spacing: 0.16em; font-size: 0.85rem;
  color: var(--orange);
  opacity: 0; transition: opacity 0.15s ease;
}
.sidebar-role.visible { opacity: 1; }
.bio-fragments {
  display: flex; flex-direction: column; gap: 1.1rem; overflow-y: auto;
  scrollbar-width: none; /* Firefox */
  -ms-overflow-style: none; /* older Edge/IE */
}
.bio-fragments::-webkit-scrollbar { display: none; /* Chrome/Safari/new Edge */ }
.bio-fragment {
  margin: 0; font-size: 1.05rem; font-weight: 700; line-height: 1.4;
  opacity: 0; transform: translateY(10px); transition: opacity 0.5s ease, transform 0.5s ease;
}
.bio-fragment.revealed { opacity: 1; transform: translateY(0); }

/* Dot nav */
.dot-nav {
  position: fixed; right: 1.5rem; top: 50%; transform: translateY(-50%);
  display: flex; flex-direction: column; gap: 0.6rem; z-index: 80;
}
.dot {
  width: 10px; height: 10px; border-radius: 50%; border: 2px solid var(--black);
  background: transparent; padding: 0; cursor: pointer; transition: 0.2s ease;
}
.dot:hover { background: rgba(5, 5, 5, 0.2); }
.dot.active { background: var(--orange); border-color: var(--orange); transform: scale(1.4); }

/* Scroll engine */
.scroll-outer { position: relative; height: calc(var(--panel-count, 14) * 100vh); z-index: 1; }
.scroll-viewport { position: sticky; top: 0; height: 100vh; overflow: hidden; touch-action: pan-y; }
.track { display: flex; height: 100%; will-change: transform; }

.panel {
  width: 100vw; height: 100%; flex-shrink: 0; overflow: hidden;
  display: flex; align-items: center;
  padding: var(--content-clearance) 4rem 3rem calc(var(--bio-width) + 4rem);
}
.panel-inner { width: 100%; }

/* Intro panel */
.panel--intro .panel-inner { max-width: 900px; }
.intro-eyebrow { font-size: clamp(0.8rem, 1.2vw, 1rem); font-weight: 900; text-transform: uppercase; letter-spacing: 0.28em; margin: 0 0 1.4rem; }
.intro-title { margin: 0; font-weight: 900; text-transform: uppercase; letter-spacing: -0.06em; line-height: 0.88; font-size: clamp(3.2rem, 8vw, 8.5rem); }
.pulse-word {
  display: inline-block; animation: pulseBeat 3s ease-in-out infinite;
  transform-origin: center; color: var(--orange);
  font-family: Georgia, "Times New Roman", serif; font-style: italic;
}
.intro-hint { margin: 2rem 0 0; font-weight: 900; text-transform: uppercase; letter-spacing: 0.2em; font-size: 0.9rem; opacity: 0.6; }

/* Project panels */
.panel--project .panel-inner { display: grid; grid-template-columns: 1.2fr 1fr; gap: clamp(2rem, 5vw, 4rem); align-items: center; }
.panel-media, .panel-copy { min-width: 0; }
.panel-media {
  border: var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--black);
  box-shadow: 0 30px 60px rgba(0, 0, 0, 0.2); aspect-ratio: 4 / 3;
}
.panel-media img { display: block; width: 100%; height: 100%; object-fit: cover; }
.panel-media iframe { display: block; width: 100%; height: 100%; border: 0; }

/* Embedded live-site panels: a large iframe with a floating caption card,
   so the site itself gets much more room instead of sharing the row with text. */
.panel--embed .panel-inner { display: flex; align-items: center; justify-content: center; height: 100%; }
/* Width is capped a second way, via an aspect-ratio-derived vh value, so a wide-but-short
   viewport shrinks the frame to fit the panel's padded box instead of centering it into overflow. */
.embed-frame { position: relative; width: min(1200px, 94%, calc((100vh - var(--content-clearance) - 3rem) * 1.6)); margin: 0 auto; }
.panel--embed .panel-media { aspect-ratio: 16 / 10; }
.panel-card {
  position: absolute; left: 2rem; bottom: 2rem; max-width: 400px;
  background: rgba(5, 5, 5, 0.58); backdrop-filter: blur(18px); -webkit-backdrop-filter: blur(18px);
  color: var(--cream); border: var(--border); border-radius: var(--radius-md);
  padding: 1.75rem; display: flex; flex-direction: column; align-items: flex-start; gap: 0.85rem;
  box-shadow: 0 24px 50px rgba(0, 0, 0, 0.35);
}
.panel-card .panel-title { color: var(--cream); font-size: clamp(1.6rem, 2.2vw, 2.2rem); }
.panel-card .panel-desc { color: var(--cream); opacity: 0.85; font-size: 0.95rem; }
.panel-card .panel-link { background: var(--cream); color: var(--black); border-color: var(--cream); }
.panel-card .panel-link:hover { background: var(--orange); border-color: var(--orange); color: var(--black); }

/* Grub's Up: cover image, fans out to the app screens on hover/tap */
.grubsup-media {
  position: relative; display: block; width: 100%; padding: 0; cursor: pointer;
  font: inherit; color: inherit;
}
.grubsup-cover { transition: opacity 0.3s ease; }
.grubsup-fan {
  position: absolute; inset: 0; display: grid; grid-template-columns: repeat(3, 1fr); grid-template-rows: repeat(2, 1fr);
  gap: 0.5rem; padding: 0.75rem; background: var(--black);
  opacity: 0; pointer-events: none; transition: opacity 0.3s ease;
}
.grubsup-fan img { width: 100%; height: 100%; object-fit: cover; object-position: top; border-radius: 10px; border: 2px solid var(--cream); }
.grubsup-hint {
  position: absolute; left: 50%; bottom: 1rem; transform: translateX(-50%);
  background: var(--black); color: var(--cream); padding: 0.6rem 1.1rem; border-radius: 999px;
  font-weight: 900; text-transform: uppercase; letter-spacing: 0.12em; font-size: 0.72rem;
  opacity: 0.9; transition: opacity 0.3s ease;
}
.grubsup-media:hover .grubsup-cover, .grubsup-media:focus-visible .grubsup-cover, .grubsup-media.active .grubsup-cover { opacity: 0; }
.grubsup-media:hover .grubsup-fan, .grubsup-media:focus-visible .grubsup-fan, .grubsup-media.active .grubsup-fan { opacity: 1; pointer-events: auto; }
.grubsup-media:hover .grubsup-hint, .grubsup-media:focus-visible .grubsup-hint, .grubsup-media.active .grubsup-hint { opacity: 0; }

.panel-copy { display: flex; flex-direction: column; align-items: flex-start; gap: 1rem; min-width: 0; }
.panel-title, .panel-desc { max-width: 100%; }
.panel-tag {
  display: inline-flex; font-size: 0.75rem; font-weight: 900; text-transform: uppercase; letter-spacing: 0.2em;
  padding: 0.5rem 1rem; border: 2px solid var(--black); border-radius: 999px;
  color: var(--black); /* tag backgrounds are always light pastels, so text must stay dark regardless of ancestor colour */
}
.panel[data-colour="lime"] .panel-tag { background: var(--lime); }
.panel[data-colour="yellow"] .panel-tag { background: var(--yellow); }
.panel[data-colour="pink"] .panel-tag { background: var(--pink); }
.panel[data-colour="sky"] .panel-tag { background: var(--sky); }
.panel[data-colour="orange"] .panel-tag { background: var(--orange); }
.panel[data-colour="mint"] .panel-tag { background: var(--mint); }

.panel-title { margin: 0; font-weight: 900; text-transform: uppercase; letter-spacing: -0.05em; line-height: 0.95; font-size: clamp(2.2rem, 4vw, 4rem); }
.panel-desc { margin: 0; font-size: clamp(1rem, 1.4vw, 1.25rem); font-weight: 500; line-height: 1.5; max-width: 48ch; }

.panel-link, .panel-platforms {
  display: inline-flex; align-items: center; border: 4px solid var(--black); border-radius: 999px;
  padding: 0.85rem 1.6rem; font-weight: 900; text-transform: uppercase; letter-spacing: 0.15em;
  font-size: 0.85rem; transition: 0.2s ease;
}
.panel-link { background: var(--black); color: var(--cream); }
.panel-link:hover { background: var(--orange); border-color: var(--orange); color: var(--black); }
.panel-platforms { background: transparent; color: var(--black); opacity: 0.7; }

/* Contact panel */
.panel--contact .panel-inner { max-width: 700px; }
.panel--contact .panel-title { font-size: clamp(2.8rem, 6vw, 6rem); margin: 1rem 0 2rem; }
.panel--contact .contact-btn {
  display: inline-flex; border: 4px solid var(--orange); background: var(--orange); color: var(--black);
  border-radius: 999px; padding: 1.1rem 2.2rem; font-weight: 900; text-transform: uppercase;
  letter-spacing: 0.2em; transition: 0.2s ease;
}
.panel--contact .contact-btn:hover { background: var(--black); border-color: var(--black); color: var(--cream); }
.contact-links { display: flex; gap: 1rem; flex-wrap: wrap; margin-top: 2rem; }
.contact-links a {
  border: 3px solid var(--black); border-radius: 999px; padding: 0.7rem 1.3rem;
  font-weight: 900; text-transform: uppercase; letter-spacing: 0.12em; font-size: 0.85rem; transition: 0.2s ease;
}
.contact-links a:hover { background: var(--black); color: var(--cream); }

@keyframes pulseBeat { 0%, 100% { transform: rotate(-4deg) scale(1); text-shadow: none; } 45% { transform: rotate(-4deg) scale(1.08); text-shadow: 0 0 0.18em rgba(255, 122, 26, 0.75); } 65% { transform: rotate(-4deg) scale(0.98); } }

@media (max-width: 900px) {
  /* No page-level scroll at all on mobile — the carousel below is its own fixed,
     self-contained horizontally-swiping strip, so there's nothing to scroll past it. */
  body { overflow: hidden; height: 100%; }

  /* Header becomes a fixed-height bar so the bio bar below it can dock precisely underneath. */
  .site-header { height: 3.25rem; padding: 0 1rem; font-size: 0.65rem; letter-spacing: 0.08em; }
  .role-track { display: none; }
  .site-name.is-hidden { opacity: 1; transform: none; } /* the name/mark swap is a desktop-only flourish */
  .sidebar-name { display: none; }
  .sidebar-role { display: none; }

  /* Quote becomes a plain fade, pinned right below the bio bar and centered above
     the swiped panel's iframe — no slide, no header/sidebar leap choreography
     (that's a desktop-only flourish). Driven by an IntersectionObserver watching
     the natively-swiped panels (see initMobileCarousel in main.js), not the
     scroll-hijack progress math desktop uses. */
  .panel-quote {
    top: 11.5rem; left: 0; right: 0; padding: 0 1.5rem;
    transition: opacity 0.35s ease;
  }
  .panel-quote-text { font-size: 1rem; max-width: 34ch; }
  .panel-quote-author { font-size: 0.62rem; }

  /* Bio panel moves from a left rail to a top bar: mark centered on its own row
     (this is where the portrait photo will go), with a boxed caption underneath
     showing only the current fragment — no room to stack a growing list here. */
  .bio-panel {
    top: 3.25rem; left: 0; right: 0; width: 100%; height: auto;
    flex-direction: column; align-items: center; gap: 0.6rem;
    padding: 0.85rem 1rem; border-right: none; border-bottom: var(--border);
  }
  .bio-mark { width: 44px; height: 44px; }
  .bio-fragments {
    width: 100%; max-width: 420px; text-align: center;
    border: 2px solid var(--black); border-radius: var(--radius-md);
    padding: 0.55rem 0.9rem; background: var(--cream); overflow: visible;
  }
  .bio-fragment { display: none; opacity: 1; transform: none; margin: 0; font-size: 0.78rem; line-height: 1.3; }
  .bio-fragment.current {
    display: -webkit-box; -webkit-line-clamp: 2; line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
  }

  .dot-nav { right: 0.5rem; gap: 0.4rem; }
  .dot { width: 8px; height: 8px; }

  /* Scroll engine: a plain native horizontally-swiping, scroll-snapped strip —
     pinned to fill the screen below the fixed header/bio bar, its own contained
     scroller — instead of desktop's scroll-hijack transform math. The browser
     handles the actual swipe; main.js just watches which panel snapped into
     view (see initMobileCarousel) to sync the bio bar, dot-nav and quote. */
  .scroll-outer { height: 100vh; }
  .scroll-viewport {
    position: fixed; inset: 0; overflow-x: auto; overflow-y: hidden;
    scroll-snap-type: x mandatory; -webkit-overflow-scrolling: touch;
    overscroll-behavior-x: contain; touch-action: pan-x; /* base rule is pan-y, which would block this native horizontal swipe */
  }
  .track { will-change: auto; }
  /* Fixed top offset — covers header + bio bar + quote strip at their tallest
     (a two-line bio fragment alongside a full two-line quote, measured at
     ~290px) plus a small buffer, so the iframe/screenshot box below always
     sits in the same spot regardless of which quote is showing. Trimmed as
     tight as that measurement safely allows, since the media below is now
     bigger and needs the room. */
  .panel {
    padding: 19rem 1.25rem 1.5rem;
    scroll-snap-align: start; scroll-snap-stop: always;
  }
  .panel--project .panel-inner { grid-template-columns: 1fr; gap: 1.5rem; }
  .panel-desc { max-width: none; }

  /* Top-aligned rather than centered: on a very short viewport, centered content
     that's taller than the available box bleeds upward past the padding-top edge
     into the quote strip above it. Top-aligned, any overflow clips safely at the
     bottom instead (panel already has overflow: hidden). */
  .panel--embed, .panel--project { align-items: flex-start; }
  .panel--embed .panel-inner { display: block; height: auto; }
  .embed-frame { display: flex; flex-direction: column; gap: 0.75rem; width: 100%; }
  /* Iframes made bigger to match the Grub's Up screenshot's box size (4:3, not
     the old flatter 16:7), so every panel's hero media reads as one consistent,
     properly-sized shape while swiping through. */
  .panel--embed .panel-media, .grubsup-media { aspect-ratio: 4 / 3; }
  .panel-card { position: static; max-width: none; width: 100%; box-shadow: none; padding: 1rem; gap: 0.5rem; }
  .panel-card .panel-desc {
    font-size: 0.85rem; line-height: 1.4;
    display: -webkit-box; -webkit-line-clamp: 2; line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
  }
}

@media (max-width: 480px) {
  .intro-title { font-size: clamp(2.1rem, 12vw, 3.2rem); }
  .panel-title { font-size: clamp(1.7rem, 9vw, 2.4rem); }
  .panel-desc { font-size: 0.95rem; }
  .panel-tag { font-size: 0.65rem; padding: 0.4rem 0.8rem; }
  .panel-link, .panel-platforms { font-size: 0.75rem; padding: 0.7rem 1.3rem; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; }
}
