/* ============================================
   1. RESET & BASE
   ============================================ */
* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

html {
  /* This one line is what makes clicking a nav link glide to the
     section instead of jumping instantly. */
  scroll-behavior: smooth;
  /* Without this, phones in system dark mode render the page's
     unstyled canvas (the overscroll/bounce area beyond the body, and
     anything before the stylesheet paints) black instead of this
     site's light design — color-scheme tells the browser this page is
     light-only, so it stops following the OS setting there. The
     <meta name="color-scheme"> tag in each page's <head> does the same
     thing before the CSS has even loaded. */
  color-scheme: light;
  background-color: #faf9f6;
}

body {
  font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
  color: #1a1a1a;
  background-color: #faf9f6;
  /* Delicate dot-grain texture: a single 1px dot per tile, repeated
     every 22px, at low opacity — reads as a subtle paper texture
     rather than a visible pattern. Pure CSS (no image file), so it
     stays crisp at any zoom/density and tiles seamlessly down the
     page. */
  background-image: radial-gradient(circle at 1px 1px, rgba(26, 26, 26, 0.06) 1px, transparent 0);
  background-size: 22px 22px;
  line-height: 1.6;
}

/* ============================================
   2. COLOR & TYPE TOKENS
   Change these once, and the whole site updates.
   ============================================ */
:root {
  --ink: #1a1a1a;
  --ink-hover: #4e4e4e;
  --paper: #faf9f6;
  --index: #eeece6;
  --accent: #2455e6;      /* deep pine green — swap for your brand color */
  --muted: #6b6b66;
  --button: rgba(250, 249, 246, 0.3);
  --button-hover: rgba(250, 249, 246, 0.5);
  --link: #2455e6;
  --link-hover: #143bae;
  --display-font: 'Helvetica Neue', Helvetica, Arial, sans-serif;
  --carousel-bg-plots:#363636;
  --carousel-bg-white:#FFFFFF;
  --carousel-bg-grey:#E6E6E6;
  --image-radius: 4px;    /* project cards, hero/figure/carousel images, lightbox */
}

/* ============================================
   3. HEADER + INDEX NAV
   ============================================ */

/* Hides text visually but keeps it for screen readers + SEO.
   Used on the H1/H2 that the logo image now visually replaces. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
}

.site-header {
  position: sticky;
  top: 0;
  /* Above the About page's hovered badges and tooltips (z-index 30-40 in
     their own blocks), so they slide behind the header, not over it. Still
     below the lightbox and toast (100). */
  z-index: 50;
  background-color: rgba(250, 249, 246, 0.75);
  backdrop-filter: blur(12px) saturate(180%);
  -webkit-backdrop-filter: blur(12px) saturate(180%);
  padding: 1.25rem 1.5rem;
  box-shadow: 0 8px 16px -8px rgba(0, 0, 0, 0.06);
  
  /* .scrolled is toggled by script.js once you're past the very top —
     shrinks the header (and the logo, below) so it takes up less room
     while you're reading, then relaxes back to full size at the top. */
  transition: padding 0.25s ease;
}

.site-header.scrolled {
  padding: 0.4rem 1.5rem;
}

@media (prefers-reduced-motion: reduce) {
  .site-header {
    transition: none;
  }
}

/* Frosted glass needs a solid fallback for reduced-transparency and for
   browsers without backdrop-filter support, so content underneath never
   reads illegibly through the header. */
@supports not (backdrop-filter: blur(1px)) {
  .site-header {
    background-color: var(--paper);
  }
}

@media (prefers-reduced-transparency: reduce) {
  .site-header {
    background-color: var(--paper);
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    border-bottom: 1px solid #e4e2db;
  }
}

.header-row {
  position: relative;
  display: flex;
  align-items: center;
  min-height: 40px;
}

.index-nav {
  display: flex;
  align-items: center;
  margin-left: auto;
}

.nav-links {
  list-style: none;
  display: flex;
  align-items: center;
  gap: 3rem;
  flex-wrap: wrap;
  margin-right: 3rem;
}

.index-nav a {
  position: relative;
  display: inline-block;
  color: var(--ink);
  text-decoration: none;
  font-weight: 400;
  font-size: 0.85rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding-bottom: 2px;
  transition: color 0.2s ease;
}

/* Hovering (or keyboard-focusing) any top-menu link turns it the dark
   link blue, on top of the underline sweep. */
.index-nav a:hover,
.index-nav a:focus-visible {
  color: var(--link-hover);
}

/* The top-menu link for the page you're already on: a little bolder
   (600 renders as bold in fonts without a semi-bold, so it reads on any
   device). Clicking it scrolls back to the top — see script.js. Hover
   color is shared by every top-menu link, see .index-nav a:hover. */
.index-nav a[aria-current="page"] {
  font-weight: 500;
}

.highlight-link{
  color: var(--accent) !important;
}

/* The accent color above is !important, so the hover color has to be too
   (and more specific) or it never shows. Every highlight link — "About
   me →", "← Projects", "Next project: …", "Next case study: …" — turns
   the same dark link blue as the top-menu links on hover/focus, with the
   underline sweep in that blue as well. */
.highlight-link {
  transition: color 0.2s ease;
}

.highlight-link:hover,
.highlight-link:focus-visible {
  color: var(--link-hover) !important;
}

.highlight-link .underline-hover-overlay {
  color: var(--link-hover);
}

/* Logo stays perfectly centered regardless of how wide the nav is.
   Stays position: absolute at every width (rather than switching to
   static on mobile) specifically so left/transform can transition
   smoothly as the viewport crosses the 900px breakpoint — animating
   between two different position values isn't possible, but animating
   left/transform within the same positioning scheme is. */
.logo-link {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  transition: left 0.3s ease, transform 0.3s ease;
}

@media (prefers-reduced-motion: reduce) {
  .logo-link {
    transition: none;
  }
}

.site-logo {
  width: clamp(130px, 40vw, 190px);
  height: auto;
  display: block;
  transition: width 0.25s ease;
}

.site-header.scrolled .site-logo {
  width: clamp(95px, 30vw, 135px);
}

@media (prefers-reduced-motion: reduce) {
  .site-logo {
    transition: none;
  }
}

/* Hamburger button: hidden on desktop, shown only on mobile (see media query) */
.menu-toggle {
  display: none;
  background: none;
  border: none;
  padding: 0.25rem;
  cursor: pointer;
  color: var(--ink);
  position: relative;
  z-index: 11;
  transition: transform 0.1s ease-out;
}

.menu-toggle:active {
  transform: scale(0.88);
}

.menu-toggle .material-symbols-outlined {
  font-size: 28px;
  display: block;
}

.menu-toggle .menu-icon-close {
  display: none;
}

.menu-toggle[aria-expanded="true"] .menu-icon-open {
  display: none;
}

.menu-toggle[aria-expanded="true"] .menu-icon-close {
  display: block;
}

/* Mobile: no hamburger/dropdown — Projects and About stay visible
   inline, same as desktop. Logo moves to the left (out of its
   desktop centered position) and the nav links sit on the right. */
@media (max-width: 1300px) {
  .menu-toggle {
    display: none;
  }

  .logo-link {
    left: 1rem;
    transform: translateY(-50%);
  }

  .nav-links {
    gap: 2rem;
    margin-right: 1rem;
  }
}

@media (max-width: 520px) {
  .logo-link {
    left: 0rem;
  }

  .nav-links {
    margin-right: 0rem;
  }
}

/* Very narrow screens (under 320px): there's no room for Projects and
   About inline next to the logo, so they collapse into the hamburger.
   script.js toggles .open on .nav-links. The dropdown is a full-width
   glass panel hanging off the header's bottom edge (1.25rem is the
   header's bottom padding, 0.4rem once it has shrunk on scroll; the
   -1.5rem left/right cancel the header's side padding so it spans the
   whole screen). It's hidden with visibility as well as opacity so its
   links aren't tabbable while closed.

   A backdrop-filter element only blurs what's behind it *up to* the
   nearest ancestor that itself has a backdrop-filter — so with the
   glass on .site-header, the dropdown inside it would blur nothing but
   the header. To let it blur the page, the header's own glass moves
   onto a ::before layer here (which isn't an ancestor of the
   dropdown). */
@media (max-width: 319.98px) {
  .site-header {
    background-color: transparent;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }

  .site-header::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    background-color: rgba(250, 249, 246, 0.75);
    backdrop-filter: blur(12px) saturate(180%);
    -webkit-backdrop-filter: blur(12px) saturate(180%);
  }

  .menu-toggle {
    display: block;
  }

  .nav-links {
    position: absolute;
    top: calc(100% + 1.25rem);
    left: -1.5rem;
    right: -1.5rem;
    flex-direction: column;
    align-items: stretch;
    flex-wrap: nowrap;
    gap: 0;
    margin: 0;
    padding: 0.5rem 0;
    background-color: rgba(250, 249, 246, 0.75);
    backdrop-filter: blur(12px) saturate(180%);
    -webkit-backdrop-filter: blur(12px) saturate(180%);
    box-shadow: 0 8px 16px -8px rgba(0, 0, 0, 0.06);
    /* Closed state, and the closing transition: the links fade first
       (see .nav-links a), then the panel retracts up into the header
       with a short delay. Revealed/retracted with clip-path rather
       than just fading, so it reads as unrolling from the header. */
    opacity: 0;
    visibility: hidden;
    clip-path: inset(0 0 100% 0);
    transform: translateY(-8px);
    transition:
      clip-path 0.5s cubic-bezier(0.4, 0, 0.2, 1) 0.06s,
      opacity 0.4s ease 0.12s,
      transform 0.5s cubic-bezier(0.4, 0, 0.2, 1) 0.06s,
      visibility 0s linear 0.56s;
  }

  .site-header.scrolled .nav-links {
    top: calc(100% + 0.4rem);
  }

  /* Opening: a longer, softer ease-out. Same curve family as the
     site's scroll reveals (0.2, 0.8, 0.2, 1). */
  .nav-links.open {
    opacity: 1;
    visibility: visible;
    clip-path: inset(0 0 0 0);
    transform: translateY(0);
    transition:
      clip-path 0.55s cubic-bezier(0.2, 0.8, 0.2, 1),
      opacity 0.4s ease,
      transform 0.55s cubic-bezier(0.2, 0.8, 0.2, 1),
      visibility 0s linear 0s;
  }

  .nav-links a {
    display: block;
    padding: 0.9rem 1.5rem;
    opacity: 0;
    transform: translateY(-8px);
    transition: opacity 0.22s ease, transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  }

  /* The links settle in one after the other, just behind the panel. */
  .nav-links.open a {
    opacity: 1;
    transform: translateY(0);
    transition: opacity 0.45s ease 0.12s, transform 0.55s cubic-bezier(0.2, 0.8, 0.2, 1) 0.12s;
  }

  .nav-links.open li:nth-child(2) a {
    transition-delay: 0.2s;
  }
}

@media (max-width: 319.98px) and (prefers-reduced-motion: reduce) {
  .nav-links,
  .nav-links.open,
  .nav-links a,
  .nav-links.open a {
    transition: none;
  }
}

/* Same solid fallbacks as the header's, for the layers that carry the
   glass at this width. */
@supports not (backdrop-filter: blur(1px)) {
  @media (max-width: 319.98px) {
    .site-header::before,
    .nav-links {
      background-color: var(--paper);
    }
  }
}

@media (max-width: 319.98px) and (prefers-reduced-transparency: reduce) {
  .site-header::before,
  .nav-links {
    background-color: var(--paper);
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }
}

/* ============================================
   4. SECTIONS
   scroll-margin-top keeps a section from being hidden
   under a fixed header when jumped to — increase this
   if you add a sticky nav bar later.
   ============================================ */
.section {
  max-width: 1200px; /* 960px */
  margin: 0 auto;
  /*padding: 5rem 1.5rem;*/
  scroll-margin-top: 2rem;
  /*padding-bottom: 1.5rem;*/
  /* Horizontal gutter — was folded into the (now commented-out)
     shorthand above, so it disappeared along with it. Doesn't show at
     wide viewports since max-width already centers the section with
     room to spare, but below ~1200px the section fills the full
     width and content ran edge-to-edge without this. */
  padding-left: 1.5rem;
  padding-right: 1.5rem;
}

.section-about {
  padding-top: 2rem;
}

.section h2 {
  font-family: var(--display-font);
  font-size: 2rem;
  line-height: 1.15;
  margin-bottom: 2rem;
}

/* Homepage tagline: types out "I love creating <word>!" on load, then
   loops — backspacing only the word (never the fixed "I love creating"
   part) and typing the next one in from script.js's TYPEWRITER_WORDS.
   aria-hidden since a screen reader reading out every keystroke would
   be unusable; the .visually-hidden sibling right after it in the
   markup gives screen readers the same sentence as a single static
   read instead. */
.typewriter-cursor {
  animation: typewriter-blink 1s step-end infinite;
}

@keyframes typewriter-blink {
  0%, 50% { opacity: 1; }
  50.01%, 100% { opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .typewriter-cursor {
    animation: none;
  }
}

/* ============================================
   5. PROJECT GRID
   ============================================ */
.project-grid {
  list-style: none;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: 2rem;
  margin-bottom: 2rem;
}



.project-card a {
  display: block;
  text-decoration: none;
  color: var(--ink);
}

.project-image {
  position: relative;
  overflow: hidden;
  border-radius: var(--image-radius);
  aspect-ratio: 1 / 1;
  background-color: #ece9e2; /* shows while the image loads */
}

.project-image img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform 0.4s ease;
}

/* A card's cover can also be a short video: it sits over the picture,
   invisible, and fades in (and plays, see script.js) while the card is
   hovered. Mirrors the picture's hover zoom so the two move together. */
.cover-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.3s ease;
}

.project-card a:hover .cover-video,
.project-card a:focus-visible .cover-video {
  opacity: 1;
}

/* Holds the picture and the video. Unlike the other cards this one has no
   hover zoom: the video fades in over the still at the same size, so
   nothing appears to move except the video itself. */
.cover-media {
  position: relative;
  display: block;
  width: 100%;
  height: 100%;
}

/* No zoom on this card's picture (this needs to out-rank the plain-card
   rule that zooms .project-image img on hover). */
.project-card a .project-image .cover-media img,
.project-card a:hover .project-image .cover-media img,
.project-card a:focus-visible .project-image .cover-media img {
  transform: none;
}

/* Phones and touch screens have no hover, so the video is always showing
   and script.js plays it while the card is on screen. It sits exactly over
   the picture, which shows until the first frame is ready. */
@media (hover: none), (max-width: 680px) {
  .cover-video {
    opacity: 1;
    border-radius: var(--image-radius);
  }
}

@media (prefers-reduced-motion: reduce) {
  .cover-video {
    display: none;
  }
}

/* The caption sits on top of the image, invisible until hovered */
.project-overlay {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  padding: 1.25rem;
  background: linear-gradient(to top, rgba(0, 0, 0, 0.85) 0%, rgba(0, 0, 0, 0.5) 60%, rgba(0, 0, 0, 0) 95%);
  opacity: 0;
  transition: opacity 0.3s ease;
}

/* Below 680px there's no hover to reveal the caption, so it moves
   from an absolutely-positioned overlay ON the image (clipped to a
   fixed aspect-ratio box) to plain content BELOW it instead — the
   aspect-ratio moves from the wrapping div onto the image itself so
   the image stays square while the div's height goes back to auto,
   letting the caption sit underneath in normal flow rather than
   being clipped by the div's own overflow:hidden. */
@media (max-width: 680px) {
  .project-image {
    aspect-ratio: auto;
    /*aspect-ratio: 5 / 4;*/
    overflow: visible;
    border-radius: 0;
  }

  .project-image img {
    /*aspect-ratio: 1 / 1;*/
    aspect-ratio: 5 / 4;
    height: auto;
    border-radius: var(--image-radius);
  }

  .project-overlay {
    position: static;
    display: block;
    padding: 0.8rem 0 0.5rem;
    background: var(--paper);
    opacity: 1;
  }
}



.project-overlay h3 {
  font-family: var(--display-font);
  font-size: 1.15rem;
  color: #fff;
}

.project-overlay .project-meta {
  font-size: 0.85rem;
  color: #f0efe9;
  margin-top: 0.25rem;
}

/* White text made sense on the dark image-gradient overlay above —
   once the caption moves below the image as plain page content, it
   needs to switch to the site's normal dark-on-light text colors
   instead. Declared after the white base rules above (same
   specificity either way) so this wins at ≤680px. */
@media (max-width: 680px) {
  .project-overlay h3 {
    color: var(--ink);
  }

  .project-overlay .project-meta {
    color: var(--muted);
  }
}
.divider {
  padding-bottom: 2rem;
  border-bottom: 1px solid rgb(228, 226, 219);
}

.project-card a:hover .project-image img,
.project-card a:focus-visible .project-image img {
  transform: scale(1.02);
  
}

.project-card a:hover .project-overlay,
.project-card a:focus-visible .project-overlay {
  opacity: 1;
}

/* Homepage cards and Civi's case-study cards: while a card is hovered its
   cover drifts a little away from the pointer (script.js sets --mx / --my,
   in px), like a picture sitting slightly behind the frame. The extra
   scale keeps the edges from showing as it slides. The Motion card is
   left out: its video already moves, and its picture (inside
   .cover-media) is kept still below. */
@media (hover: hover) and (min-width: 681px) {
  .section-projects .project-card .project-image img,
  .case-study-card .project-image img {
    transition: transform 0.6s cubic-bezier(0.2, 0.8, 0.2, 1);
  }

  .section-projects .project-card a:hover .project-image img,
  .section-projects .project-card a:focus-visible .project-image img,
  .case-study-card a:hover .project-image img,
  .case-study-card a:focus-visible .project-image img {
    transform: translate(var(--mx, 0px), var(--my, 0px)) scale(1.06);
  }

  /* The Motion card's picture stays put. */
  .section-projects .project-card a:hover .project-image .cover-media img,
  .section-projects .project-card a:focus-visible .project-image .cover-media img {
    transform: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .project-image img,
  .cover-media,
  .project-overlay {
    transition: none;
  }
}

/* Case-study index cards (e.g. Civi's "Case Studies" section): reuses
   .project-card/.project-image for the thumbnail + hover zoom, but the
   title/description sit as plain visible text below the image instead
   of .project-overlay's hover-only caption — these are meant to be
   scanned and read directly, not discovered on hover. Fixed 3-column
   layout (vs. the homepage grid's auto-fill) since this always shows
   the same fixed set of cards, collapsing to fewer columns as the
   project page's content column narrows. */
.case-study-grid {
  grid-template-columns: repeat(3, 1fr);
  margin-top: 3rem !important;
}

@media (max-width: 900px) {
  .case-study-grid {
    grid-template-columns: repeat(2, 1fr);
  }
}

@media (max-width: 600px) {
  .case-study-grid {
    grid-template-columns: 1fr;
  }
}

/* 3:2 instead of .project-image's default square — reads better for
   screenshots/diagrams than a 1:1 crop. Overrides the mobile rule
   below 680px too, which otherwise switches .project-image to
   aspect-ratio: auto and puts a different ratio on the image itself. */
.case-study-card .project-image,
.case-study-card .project-image img {
  aspect-ratio: 5 / 4;
}

.case-study-card h3 {
  margin-top: 0.8rem;
  font-size: 1.05rem;
}

.case-study-card p {
  margin-top: 0.35rem;
  font-size: 0.9rem;
  color: var(--muted);
}

/* ============================================
   6. FOOTER
   ============================================ */
.site-footer {
  text-align: center;
  padding: 2rem;
  color: var(--muted);
  font-size: clamp(0.9rem, 3vw, 1rem) !important;
}

.site-footer-small {
  font-size: clamp(0.7rem, 3vw, 0.8rem) !important;
  /* Lighter than the credit line above it (which keeps --muted) — same
     grey, just faded, so the copyright/rights line reads as the least
     important text on the page. */
  color: rgba(107, 107, 102, 0.6);
}

.footer-line-break{ display: none; }

@media (max-width: 700px) {
  .footer-line-break{ display: block; }
}

/* Social icons sit above the credit lines (markup lives in footer.js).
   The 16px padding on each icon's box (see .social-links a) reaches
   past the row, so the bottom margin is a little larger than it looks. */
.site-footer .social-links {
  margin-bottom: 2.5rem;
}

@media (max-width: 760px) {
  .site-footer {
    align-items: left;
    text-align: left;
    padding-right: 5rem;
  }

  /* Follows the left-aligned text, still starting flush with it. More
     breathing room between icons than before (2rem) — the row reclaims
     the padding-right above (which is only there to clear the
     back-to-top button) as extra width, so the wider gap has room to
     spread into on the right without wrapping to a second line. */
  .site-footer .social-links {
    justify-content: flex-start;
    gap: 3rem;
    width: calc(100% + 5rem);
  }
}

/* ============================================
   7a. LIGHTBOX (click-to-zoom on project images)
   ============================================ */
.project-hero img,
.project-figure img {
  cursor: zoom-in;
}

.lightbox {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: rgba(0, 0, 0, 0.92);
  padding: 4.5rem 1.5rem;
  opacity: 0;
  visibility: hidden;
  /* Nothing in here is meant to be text-selected — a stray click-drag
     (easy to do reaching for the zoom/nav buttons) was highlighting
     the image in the browser's selection blue instead of doing
     anything useful. */
  user-select: none;
  -webkit-user-select: none;
  /* Hands every touch gesture inside the lightbox (pinch, drag, swipe)
     to our own touchstart/move/end handlers in script.js instead of
     the browser's native page-zoom/scroll — without this, a pinch here
     zoomed the whole page (dragging the fixed backdrop/buttons along
     with it) and a drag could scroll the page behind the overlay. */
  touch-action: none;
  /* visibility has its own 0s transition, delayed until the opacity
     fade finishes closing — without this, visibility (which can't be
     animated) snaps to hidden the instant .open is removed, cutting
     off the fade-out (backdrop, controls and image) before it gets a
     chance to actually play. */
  transition: opacity 0.45s ease, visibility 0s linear 0.45s;
}

.lightbox.open {
  opacity: 1;
  visibility: visible;
  transition: opacity 0.35s ease, visibility 0s linear 0s;
}

.lightbox-img-wrap {
  width: 100%;
  height: 100%;
  display: flex;
  /* "safe center" keeps the image centered while it fits, but falls
     back to start-aligned once it's bigger than the wrap — so zooming
     in doesn't clip the overflow on the far side out of scroll reach.
     Declared twice: browsers that don't understand "safe" (an older
     Safari) ignore that whole line and keep the plain "center" above
     it instead of dropping alignment entirely. */
  align-items: center;
  align-items: safe center;
  justify-content: center;
  justify-content: safe center;
  overflow: auto;
}

.lightbox-img-wrap img {
  max-width: 100%;
  max-height: 100%;
  width: auto;
  height: auto;
  border-radius: var(--image-radius);
  cursor: zoom-in;
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
  -webkit-user-drag: none;
  /* transform/opacity: grow-from-thumbnail on open, shrink-back on
     close, and the prev/next slide — script.js drives all three by
     setting a starting transform/opacity and clearing it. Zoom level
     changes (width/height) are intentionally NOT in this list — they
     apply instantly rather than animating. */
  transition: transform 0.35s cubic-bezier(0.2, 0.8, 0.2, 1), clip-path 0.35s cubic-bezier(0.2, 0.8, 0.2, 1), opacity 0.3s ease;
}

/* Some source images (icon sheets, diagram GIFs) have a genuinely
   transparent background rather than a solid one — on the dark
   lightbox backdrop that reads as a hole. script.js toggles this class
   on/off per image, driven by data-lightbox-bg="white" on the
   original <img> in the page. */
.lightbox-img-wrap img.lightbox-img-on-white {
  background-color: #ffffff;
}

@media (prefers-reduced-motion: reduce) {
  .lightbox-img-wrap img {
    transition: none;
  }
}

/* Zoomed-in state: the image is sized above its fit-to-screen size
   (script.js sets explicit width/height). display/overflow don't need
   to change here anymore — the wrap is always flex + overflow: auto
   (see above) — this just swaps the cursor and lets the explicit size
   exceed the fit-to-screen max-width/max-height. Switching display
   between flex and block here used to be the bug behind the "jumps to
   a corner" zoom animation: display isn't animatable, so centering
   vanished the instant you zoomed, before the width/height had
   actually finished growing. */
/* Set by script.js while a zoom animation runs, so the image scaled
   past its final size doesn't briefly add scrollbars. */
.lightbox-img-wrap.zoom-animating {
  overflow: hidden;
}
.lightbox-img-wrap.zoomed img {
  max-width: none;
  max-height: none;
  cursor: zoom-out;
}

.lightbox-caption {
  position: absolute;
  bottom: 1.25rem;
  left: 0;
  right: 0;
  text-align: center;
  color: #f0efe9;
  font-size: 0.85rem;
  padding: 0 4.5rem;
  /* Its box spans the full width even though the text is centered and
     padded — without this, it sits on top of (and swallows clicks on)
     anything sharing this row, like the zoom controls on desktop. */
  pointer-events: none;
}

/* Shared round icon-button look, used by the lightbox controls (close,
   prev/next, zoom in/out) and the carousel's prev/next — a translucent
   frosted-glass circle rather than a flat/bare icon, so every "button
   floating over an image" on the site reads as the same kind of
   control. Dark ink icon on a light frosted pill reads clearly against
   both the lightbox's near-black overlay and the carousel's dark
   viewport background. */
.lightbox-btn,
.carousel-prev,
.carousel-next {
  width: 44px;
  height: 44px;
  position: relative;
  border: none;
  background-color: var(--button);
  backdrop-filter: blur(8px) saturate(180%);
  -webkit-backdrop-filter: blur(8px) saturate(180%);
  color: var(--ink);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15);
  transition: transform 0.1s ease-out, background-color 0.2s ease;
  /* The close button gets focused programmatically on open (for
     keyboard users tabbing through afterward), which was showing the
     browser's default blue focus ring even though nobody "selected"
     it — the background-color change on :focus-visible below is
     already a clear enough focus indicator on its own. */
  outline: none;
}

/* Square corners to match the logo's mark (see .back-to-top,
   .carousel-dot and .lightbox-zoom for the same treatment). */
.lightbox-btn,
.carousel-prev,
.carousel-next {
  border-radius: var(--image-radius);
}

.lightbox-btn:hover,
.carousel-prev:hover,
.carousel-next:hover {
  background-color: var(--button-hover);
  color: var(--ink-hover);
}

/* Carousel prev/next specifically (not the lightbox's own buttons, which
   keep the plain frosted look above): dark-blue fill with a white icon
   — the animated hover-stroke-overlay's border picks up the same white
   for free via currentColor. */
.carousel-prev:hover,
.carousel-next:hover {
  background-color: var(--link-hover);
  color: #fff;
}

.lightbox-btn:active,
.carousel-prev:active,
.carousel-next:active {
  transform: scale(0.9);
}

.lightbox-btn:disabled {
  opacity: 0.4;
  cursor: default;
  pointer-events: none;
}

.lightbox-btn svg,
.carousel-prev svg,
.carousel-next svg {
  width: 20px;
  height: 20px;
  display: block;
}

@supports not (backdrop-filter: blur(1px)) {
  .lightbox-btn,
  .carousel-prev,
  .carousel-next {
    background-color: rgba(250, 249, 246, 0.92);
  }
}

/* The close and prev buttons sit before .lightbox-img-wrap in the DOM
   (see script.js), so without a z-index a wide or tall image — which
   paints after them — would cover the button underneath it. */
.lightbox-close {
  position: absolute;
  top: 1.25rem;
  right: 1.25rem;
  z-index: 1;
}

.lightbox-prev {
  position: absolute;
  left: 0.75rem;
  top: 50%;
  transform: translateY(-50%);
  z-index: 1;
}

.lightbox-prev:active {
  transform: translateY(-50%) scale(0.9);
}

.lightbox-next {
  position: absolute;
  right: 0.75rem;
  top: 50%;
  transform: translateY(-50%);
  z-index: 1;
}

.lightbox-next:active {
  transform: translateY(-50%) scale(0.9);
}

@media (max-width: 640px) {
  /* Hidden in favor of swipe-to-navigate (see touchstart/move/end in
     script.js) — the buttons aren't needed once swipe covers the same
     job, and they'd otherwise sit awkwardly close to the thumb. */
  .lightbox-prev,
  .lightbox-next {
    display: none;
  }
}

/* Zoom controls: bottom-center on narrow/mobile screens (centered is
   easiest to reach with a thumb); bottom-right on desktop, out of the
   way of the centered caption. Both stay above the caption's row. */
.lightbox-zoom {
  position: absolute;
  bottom: 4.5rem;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: 0.75rem;
  background-color: rgba(0, 0, 0, 0.35);
  backdrop-filter: blur(8px) saturate(180%);
  -webkit-backdrop-filter: blur(8px) saturate(180%);
  padding: 0.4rem 0.75rem;
  border-radius: var(--image-radius);
}

@media (min-width: 900px) {
  .lightbox-zoom {
    /* Same bottom offset as .lightbox-caption, so the zoom pill (right
       corner) and the caption (centered) sit on the same row instead
       of the pill floating above it. */
    bottom: 1.25rem;
    left: auto;
    right: 1.5rem;
    transform: none;
  }
}

@media (max-width: 640px) {
  /* No zoom controls on mobile at all — the click-to-zoom toggle is
     also disabled in script.js to match. */
  .lightbox-zoom {
    display: none;
  }
}

.lightbox-zoom-level {
  color: #fff;
  font-size: 0.8rem;
  min-width: 3.2em;
  text-align: center;
  font-variant-numeric: tabular-nums;
}

.lightbox-zoom .lightbox-btn {
  width: 32px;
  height: 32px;
}

.lightbox-zoom .lightbox-btn svg {
  width: 16px;
  height: 16px;
}

/* ============================================
   7b. BACK-TO-TOP (project pages only, added by script.js)
   ============================================ */
.back-to-top {
  position: fixed;
  right: 1.5rem;
  bottom: 1.5rem;
  z-index: 20;
  width: 44px;
  height: 44px;
  border-radius: var(--image-radius);
  border: none;
  /* Same frosted-glass treatment as the carousel arrows/lightbox
     controls, rather than the solid ink fill this used to have. */
  background-color: var(--button);
  backdrop-filter: blur(8px) saturate(180%);
  -webkit-backdrop-filter: blur(8px) saturate(180%);
  color: var(--ink);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15);
  opacity: 0;
  transform: translateY(8px);
  pointer-events: none;
  transition: opacity 0.25s ease, transform 0.25s ease, background-color 0.2s ease, color 0.2s ease;
}

@supports not (backdrop-filter: blur(1px)) {
  .back-to-top {
    background-color: rgba(250, 249, 246, 0.92);
  }
}

.back-to-top.visible {
  opacity: 1;
  transform: translateY(0);
  pointer-events: auto;
}

.back-to-top:hover,
.back-to-top:focus-visible {
  /* Blue/purple hover, distinct from the plain glass-brightening
     hover the other frosted buttons use — the icon flips to a light
     color to stay visible against it. Solid, not translucent — tried
     as glass and it didn't read well here. */
  background-color: var(--link-hover);
  color: var(--paper);
  /* Same lift-and-deepen-shadow hover as .badge-specs/the carousel
     controls — comes after .visible in source order so it wins their
     tied specificity and overrides its translateY(0). */
  transform: translateY(-2px);
  box-shadow: 0 10px 16px -6px rgba(0, 0, 0, 0.18);
}

.back-to-top.visible:active {
  transform: translateY(0) scale(0.9);
}

.back-to-top svg {
  width: 20px;
  height: 20px;
}

@media (prefers-reduced-motion: reduce) {
  .back-to-top {
    transition: none;
  }
}

/* ============================================
   7. ACCESSIBILITY
   ============================================ */
@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }
}

/* ============================================
   8. PROJECT PAGE (used by files inside /projects/)
   ============================================ */
.project-header {
  max-width: 1200px; /* 720px */
  margin: 0 auto;
  padding: 3rem 1.5rem 2rem;
}

.project-header-about{
  padding: 2rem 1.5rem 0 !important;
}



.project-header h1 {
  font-family: var(--display-font);
  font-size: clamp(2.5rem, 5vw, 3rem);
  line-height: 1.15;
}



/* The page H1 uses the same fade-up as the body text (.p-reveal) and the
   section H2s, so every piece of text on a page arrives the same way.
   Toggled by the same IntersectionObserver in script.js, so it replays
   every time the title scrolls out of view and back in, not just on
   first load. */
.h1-reveal {
  opacity: 0;
  transform: translateY(24px);
  transition: opacity 0.8s cubic-bezier(0.2, 0.8, 0.2, 1), transform 0.8s cubic-bezier(0.2, 0.8, 0.2, 1);
}

.h1-reveal.in-view {
  opacity: 1;
  transform: translateY(0);
}

/* Section H2s get the same fade-up as the page H1 above and the body
   copy below (.p-reveal) — same motion, same 0.8s — so all text on a
   page arrives one way. script.js adds .h2-reveal itself (rather than it living in the base h2 rule) so a
   heading only ever starts hidden once JS has actually confirmed it
   can reveal it again — if the script fails to load, h2s simply
   render normally with no animation, instead of staying invisible. */
.h2-reveal {
  opacity: 0;
  transform: translateY(24px);
  transition: opacity 0.8s cubic-bezier(0.2, 0.8, 0.2, 1), transform 0.8s cubic-bezier(0.2, 0.8, 0.2, 1);
}

.h2-reveal.in-view {
  opacity: 1;
  transform: translateY(0);
}

/* Same left-to-right reveal as the H1/H2 headings above, applied to
   each timeline logo on the About page instead — same 0.7s speed as
   the H2s, toggled by the same repeat-on-scroll IntersectionObserver
   in script.js. */
.timeline-logo-reveal {
  opacity: 0;
  transform: translateX(-24px);
  transition: opacity 0.7s cubic-bezier(0.2, 0.8, 0.2, 1), transform 0.7s cubic-bezier(0.2, 0.8, 0.2, 1);
}

.timeline-logo-reveal.in-view {
  opacity: 1;
  transform: translateX(0);
}

/* Same reveal as the H1 next to it, since they sit in the same
   opening row on the About page — same 1.6s speed, same repeat-on-
   scroll IntersectionObserver in script.js. */
.about-portrait-reveal {
  opacity: 0;
  /*transform: translateX(-24px);*/
  transition: opacity 2s cubic-bezier(0.2, 0.8, 0.2, 1), transform 2s cubic-bezier(0.2, 0.8, 0.2, 1);
  /*transition-delay: 0.2s;*/
}

.about-portrait-reveal.in-view {
  opacity: 1;
  /*transform: translateX(0);*/
}

/* Body copy (paragraphs and Problem/Solution-style sub-headings) fades
   up from below — the same motion as the H1 and H2s above, so all text
   reads as settling into place rather than arriving from off-screen.
   Same repeat-on-scroll IntersectionObserver in script.js adds/removes
   .in-view, and the same script-only setup keeps text visible by
   default if it never runs. */
.p-reveal {
  opacity: 0;
  transform: translateY(24px);
  transition: opacity 0.8s cubic-bezier(0.2, 0.8, 0.2, 1), transform 0.8s cubic-bezier(0.2, 0.8, 0.2, 1);
}

.p-reveal.in-view {
  opacity: 1;
  transform: translateY(0);
}

/* Images — hero shots, case-study figures, and the project/case-study
   grid cards — get the same fade-up as body copy, just a touch slower
   since they're heavier visual elements. Grid cards in the same row
   naturally land in view together, so they read as a row arriving at
   once rather than one at a time. */
.image-reveal {
  opacity: 0;
  transform: translateY(28px);
  transition: opacity 0.9s cubic-bezier(0.2, 0.8, 0.2, 1), transform 0.9s cubic-bezier(0.2, 0.8, 0.2, 1);
}

.image-reveal.in-view {
  opacity: 1;
  transform: translateY(0);
}

/* Homepage only (index.html sets .js-reveal in its <head>): hide the
   project cards from the very first paint. Their .image-reveal class is
   added by script.js at the end of the page, which can run after the
   browser has already painted — the cards would flash visible and skip
   the entrance animation. Same look as .image-reveal, and released the
   moment a card gets .in-view. */
@media (prefers-reduced-motion: no-preference) {
  html.js-reveal .project-card:not(.in-view) {
    opacity: 0;
    transform: translateY(28px);
  }
}

/* About and project pages (each adds .js-reveal-all to <html> in its
   <head>): the same first-paint hiding as the project cards above, for
   everything the opening animation covers. Each rule matches the hidden state of the
   element's own reveal class (.about-portrait-reveal, .h1-reveal,
   .h2-reveal, .p-reveal) exactly, so nothing jumps when script.js adds
   those classes. */
@media (prefers-reduced-motion: no-preference) {
  html.js-reveal-all .about-portrait:not(.in-view) {
    opacity: 0;
  }

  html.js-reveal-all .project-header h1:not(.visually-hidden):not(.in-view) {
    opacity: 0;
    transform: translateY(24px);
  }

  html.js-reveal-all .project-header .project-meta:not(.in-view),
  html.js-reveal-all .project-header .badge-block:not(.in-view),
  html.js-reveal-all .project-header .project-index:not(.in-view),
  html.js-reveal-all .project-header-content > .project-nav:not(.in-view),
  html.js-reveal-all main h2:not(.visually-hidden):not(.in-view),
  html.js-reveal-all main.project-body p:not(.in-view),
  html.js-reveal-all main.project-body h3:not(.in-view),
  html.js-reveal-all main.project-body h4:not(.in-view),
  html.js-reveal-all .toolkit-category:not(.in-view),
  html.js-reveal-all #specs-about-desktop .badge-block:not(.in-view),
  html.js-reveal-all #specs-about-mobile .badge-block:not(.in-view) {
    opacity: 0;
    transform: translateY(24px);
  }

  html.js-reveal-all .project-hero:not(.in-view),
  html.js-reveal-all .project-hero-static:not(.in-view),
  html.js-reveal-all .project-figure:not(.in-view) {
    opacity: 0;
    transform: translateY(28px);
  }

  html.js-reveal-all .project-carousel:not(.in-view) {
    opacity: 0;
    transform: translateY(32px);
  }
}

/* Experience and Education timelines: each entry's blocks (date badges,
   logo, text) fade up one after another when the entry scrolls into view
   — script.js adds .timeline-part to them and toggles .in-view. The id
   selectors make these win over the elements' own transition rules,
   which come later in this file (e.g. .timeline-logo-link's hover fade). */
#experience .timeline-part,
#education .timeline-part {
  opacity: 0;
  transform: translateY(24px);
  transition: opacity 0.8s cubic-bezier(0.2, 0.8, 0.2, 1), transform 0.8s cubic-bezier(0.2, 0.8, 0.2, 1);
}

#experience .timeline-part.in-view,
#education .timeline-part.in-view {
  opacity: 1;
  transform: translateY(0);
}

/* The logo's dim-on-hover, which the rules above would otherwise override. */
#experience .timeline-logo-link.timeline-part.in-view:hover,
#education .timeline-logo-link.timeline-part.in-view:hover {
  opacity: 0.8;
}

/* Carousels get the slowest, heaviest fade-up of the three — they're
   the largest single block on a page, so settling in a beat slower
   than the images above keeps the whole page's reveal rhythm feeling
   like it's building up in weight rather than everything moving at an
   identical speed. */
.carousel-reveal {
  opacity: 0;
  transform: translateY(32px);
  transition: opacity 1s cubic-bezier(0.2, 0.8, 0.2, 1), transform 1s cubic-bezier(0.2, 0.8, 0.2, 1);
}

.carousel-reveal.in-view {
  opacity: 1;
  transform: translateY(0);
}

/* An element that left through the top of the screen hides shifted up, not
   down — see the exit branch of headingObserver in script.js. Sliding
   down into place is also the natural direction when scrolling back up. */
.reveal-from-top:not(.in-view) {
  transform: translateY(-24px);
}

.project-header .project-meta {
  margin-top: 0.75rem;
  font-size: 1rem;
  color: var(--muted);
  font-size: clamp(1rem, 2vw, 1.1rem);
}

@media (max-width: 700px) {
  .project-meta-grid {
    grid-template-columns: 1fr;
    gap: 2rem;
  }
}

/* Quick-facts list (role, team, timeline, tool) under the H1 */

/* Flexbox, not grid: a CSS Grid column track (even capped with
   fit-content(N)) is sized up to its max limit whenever the grid has
   room for it, regardless of how little the wrapped badge-grid inside
   actually uses — so a short Timeline block and a long Role block
   both got stretched to their track's ceiling, leaving wildly
   different leftover whitespace before the next column despite an
   identical declared column-gap. A flex item's auto width genuinely
   hugs its content instead (shrink-to-fit), so switching to flex
   makes the visual gap between Role/Tool/Timeline match the declared
   gap exactly, every time — no per-project column-width tuning
   needed, which is why the old per-page modifier classes below are
   gone. Each block still wraps its own badges independently (rather
   than the whole block dropping to a new line) via the max-width on
   the child rule just below. */
.project-specs {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 2rem 40px; /* row-gap column-gap — 100px between Role/Tool/Timeline */
}

.project-specs > div {
  /*flex: 0 1 auto;*/
  max-width: 260px;
}

/* Float-based alternative to .project-specs' flexbox, same goal:
   each block hugs its own content and wraps internally, with a fixed
   100px gap to the next block. A floated element's width defaults to
   shrink-to-fit — the same intrinsic sizing a flex item's auto width
   gets — so .badge-block never needs a track/basis to be told to hug
   its content. Its .badge-specs children are inline-flex, which
   behaves like inline-block for line-wrapping: they flow and wrap
   within .badge-block's max-width on their own, no flex-wrap needed. */

/* Role/Tools/Timeline badges inside a page section (Motion Graphics has
   six of these). Their .badge-block children float, so without this the
   row collapses to zero height and the content after it — e.g. the
   .two-columns text + animation — gets wrapped into the thin space
   beside the floats instead of sitting below them. Scoped to
   .project-body so the header's spec blocks are unaffected. */
.project-body .project-specs-new {
  display: flow-root;
  width: 100%;
}

.badge-block {
  position: relative;
  float: left;
  margin-right: 3rem;
  margin-bottom: 1rem;
  /* A float never shrinks below its own declared width — if the
     specs column gets narrower than a block's fixed px width (the
     header starts losing room to the "On this page" box well before
     the 900px full-stack fallback below), the float just overflows
     past the column's edge instead of wrapping, which is what was
     reading as content "cut off". max-width caps it at whatever
     room is actually left, letting the float shrink like a normal
     block would; badges then wrap inside a narrower row instead of
     spilling out. Omnitrack's own #id widths already have dedicated
     breakpoints for this, so this is mostly a safety net there. */
  max-width: 100%;
  /*background-color: #ff0000;*/
}

#specs-about-desktop{
  display: flow-root; /* contains the floated .badge-block children so this element doesn't collapse to zero height */
  width: 100%;
}

#specs-about-mobile{
  display: none;
}

/* ABOUT PAGE*/
#about-based-in, #about-based-in-mobile{ width: 259px; }
#about-availability, #about-availability-mobile{ width: 194px; }
#about-languages, #about-languages-mobile{ width: 300px; }
#about-contact, #about-contact-mobile{ width: 210px; }

/* Email badge: combines the .badge-specs pill look with the
   click-to-copy behavior of .copy-email-btn (see script.js). It's a
   <button>, not an <a>, since it performs an action rather than
   navigating — .copy-email-btn's own icon-button sizing (fixed
   width/height, no padding, transparent background) would otherwise
   fight the pill's own padding/background, so these override it back
   to match .badge-specs and the toolkit .badge hover it mirrors.
   Unlike the toolkit badges, this one and the LinkedIn badge below it
   are an exception: they stay link-blue instead of ink-colored, since
   both are effectively links to reach Giovana directly. */
.badge-specs.copy-email-btn {
  width: auto;
  min-height: 36px;
  border-radius: 4px;
  background-color: var(--index);
  color: var(--link);
  box-shadow: 0 4px 8px -4px rgba(0, 0, 0, 0.1);
  /* .copy-email-btn's own transition (color/background-color only)
     would otherwise win the cascade here and drop the lift below. */
  transition: background-color 0.15s ease, color 0.15s ease, transform 0.15s ease, box-shadow 0.15s ease;
}

.badge-specs.copy-email-btn:hover,
.badge-specs.copy-email-btn:focus-visible {
  background-color: #e4e2db;
  color: var(--link-hover);
  transform: translateY(-2px);
  box-shadow: 0 10px 16px -6px rgba(0, 0, 0, 0.18);
}

/* Pressed state: drops below the resting position (not just back to
   0) and flattens the shadow almost completely, so clicking reads as
   physically pushing the pill into the page. */
.badge-specs.copy-email-btn:active {
  transform: translateY(1px);
  box-shadow: 0 1px 2px -1px rgba(0, 0, 0, 0.1);
  transition-duration: 0.05s;
}

.badge-specs.copy-email-btn .material-symbols-outlined {
  font-size: 1.05rem;
  color: var(--link);
  /* The glyph itself sits high within its own em-box — flexbox
     already centers the icon's box against the email text correctly,
     but the visible ink still reads a couple pixels too high without
     this nudge. */
  transform: translateY(1px);
}

/* The icon has its own explicit color above, so it doesn't inherit
   .copy-email-btn.copied's green — this makes the checkmark match the
   email text once it's copied. */
.badge-specs.copy-email-btn.copied .material-symbols-outlined {
  color: #1d5e29 !important;
}

#about-contact a .badge-specs,
#about-contact-mobile a .badge-specs {
  color: var(--link);
}

#about-contact a:hover .badge-specs,
#about-contact a:focus-visible .badge-specs,
#about-contact-mobile a:hover .badge-specs,
#about-contact-mobile a:focus-visible .badge-specs {
  color: var(--link-hover);
}

.split-column-about{
  width: 100%;
  height: auto;
  position: relative;
  float: left;
}

@media (max-width: 1161px) {
  #about-languages, #about-languages-mobile{ width: 420px; max-width: 100%; margin-right: 0;}
  #about-contact, #about-contact-mobile{ width: 100%; }
}

@media (max-width: 1023px) {
  #about-based-in, #about-based-in-mobile{ width: 190px; }
}

@media (max-width: 955px) {
  #about-languages, #about-languages-mobile{ width: 300px;}
}


@media (max-width: 884px) {
  #specs-about-desktop{ display: none; }

  #specs-about-mobile{
    display: grid; /* contains the floated .badge-block children so this element doesn't collapse to zero height */
    width: 100%;
      grid-template-columns: 204px auto !important;
    gap: 2rem;
  }

  #about-based-in-mobile{ width: 100% }
  /*#about-availability-mobile{ width: 100%; }
  #about-languages-mobile{ width: 100%; }*/
  #about-contact-mobile{ width: 100%; }

  .split-column-about{
    width: 100%;
    position: relative;
  }
}

@media (max-width: 700px) {
  #specs-about-mobile{
    gap: 0;
    /* Same as "auto" (as wide as its content) but never wider than the
       container — auto let the 420px language block push it past a phone. */
    grid-template-columns: minmax(0, max-content) !important;
  }
  #about-languages, #about-languages-mobile{ width: 420px; max-width: 100%;}
  #about-availability-mobile{ width: 100%; }
}

/* Omnitrack Website */
#omnitrack-website-role{ width: 146px; }
#omnitrack-website-tools{ width: 358px; }
#omnitrack-website-timeline{ width: 192px; }

@media (max-width: 1128px) {
  #omnitrack-website-tools{ width: 360px; }
  #omnitrack-website-timeline{ width: 370px; }
}

@media (max-width: 892px) {
  #omnitrack-website-role{ width: 100%; }
  #omnitrack-website-timeline{ width: 100%; }
}

@media (max-width: 840px) {
  #omnitrack-website-role{ width: 146px; }
  #omnitrack-website-tools{ width: 358px; }
  #omnitrack-website-timeline{ width: 100%; }
}

@media (max-width: 648px) {
  #omnitrack-website-role{ width: 100%; }
  #omnitrack-website-tools{ width: 358px; }
}

/* Tools is still fixed-width at this point — same overflow as
   Enkontra/Haus above, so cap it to 100% for phones too. */
@media (max-width: 640px) {
  #omnitrack-website-tools{ width: 100%; }
}

.team-block{ 
  max-width: 700px;
  margin-right: 0px !important;
}

.timeline-block{ 
  margin-right: 0px !important;
}

@media (max-width: 840px) {
  .team-block{
    max-width: auto;
    width: 100%;    
  }
}


/* Civi */
#civi-role { width: 155px; }
#civi-tools { width: 320px; }
#civi-timeline { width: 195px; }

@media (max-width: 1106px) {
  #civi-timeline { width: 100%; }
}

/*@media (max-width: 900px) {
  #civi-tools { max-width: min(450px, 100%); }
}*/

@media (max-width: 870px) {
  #civi-role { width: 100%; }
  #civi-tools { width: 520px; }
}

@media (max-width: 620px) {
  #civi-tools { width: 100%; }
}

/* Civi - Save Places */
#civi-save-places-role { width: 283px; }
#civi-save-places-tools { width: 99px; }
#civi-save-places-timeline { width: 66px; }

@media (max-width: 888px) {
  #civi-save-places-role { width: 148px; }
}
@media (max-width: 840px) {
  #civi-save-places-role { width: 283px; }
}

@media (max-width: 640px) {
  #civi-save-places-role { width: 148px; }
}

/* Civi - Design Sprint 2.0 */
#civi-design-sprint-role { width: 290px; }
#civi-design-sprint-tools { width: 195px; }
#civi-design-sprint-timeline { width: 150px; }

@media (max-width: 1067px) {
  #civi-design-sprint-role { width: 148px; }
}

@media (max-width: 1024px) {
  #civi-design-sprint-tools { width: 100px; }
  #civi-design-sprint-timeline { width: 80px; }
}

@media (max-width: 840px) {
  #civi-design-sprint-role { width: 290px; }
  #civi-design-sprint-tools { width: 195px; }
  #civi-design-sprint-timeline { width: 150px; }
}

@media (max-width: 1027px) {
  #civi-design-sprint-role { width: 148px; }
}

@media (max-width: 684px) {
  #civi-design-sprint-tools { width: 100px; }
  #civi-design-sprint-timeline { width: 80px; }
}

@media (max-width: 520px) {
  #civi-design-sprint-role { width: 100%; }
  #civi-design-sprint-tools { width: 100%; }
  #civi-design-sprint-timeline { width: 100%; }
}

/* Civi - Icons Redesign */
#civi-icons-role { width: 433px; }
#civi-icons-tools { width: 223px; }
#civi-icons-timeline { width: 66px; }

@media (max-width: 1166px) {
  #civi-icons-role { width: 284px; }
}

@media (max-width: 1018px) {
  #civi-icons-tools { width: 123px; }
}

@media (max-width: 916px) {
  #civi-icons-role { width: 150px; }
}

@media (max-width: 840px) {
  #civi-icons-role { width: 284px; }
  #civi-icons-tools { width: 223px; }
  #civi-icons-timeline { width: 66px; }
}

@media (max-width: 765px) {
  #civi-icons-tools { width: 123px; }
}

@media (max-width: 617px) {
  #civi-icons-role { width: 100%; }
  #civi-icons-tools { width: 223px; }
  #civi-icons-timeline { width: 66px; }
}

/* Civi - Submission Flow */
#civi-submission-flow-role { width: 283px; }
#civi-submission-flow-tools { width: 99px; }
#civi-submission-flow-timeline { width: 66px; }

@media (max-width: 894px) {
  #civi-submission-flow-role { width: 148px; }
}

@media (max-width: 840px) {
  #civi-submission-flow-role { width: 283px; }
}

@media (max-width: 590px) {
  #civi-submission-flow-role { width: 148px; }
}

/* Civi - Premium Plan */
#civi-premium-plan-role { width: 283px; }
#civi-premium-plan-tool { width: 192px; }
#civi-premium-plan-timeline { width: 66px; }

@media (max-width: 685px) {
  #civi-premium-plan-role { width: 148px; }
}

@media (max-width: 550px) {
  #civi-premium-plan-tool { width: 100px; }
}

/* Civi - Onboarding Redesign */
#civi-onboarding-role { width: 283px; }
#civi-onboarding-tool { width: 99px; }
#civi-onboarding-timeline { width: 113px; }

@media (max-width: 946px) {
  #civi-onboarding-role { width: 148px; }
}

@media (max-width: 840px) {
  #civi-onboarding-role { width: 283px; }
}

@media (max-width: 639px) {
  #civi-onboarding-role { width: 148px; }
}

@media (max-width: 505px) {
  #civi-onboarding-role { width: 100%; }
}

/* Civi */
#civi-marketing-role { width: 146px; }
#civi-marketing-tools { width: 280px; }
#civi-marketing-timeline { width: 174px; }

@media (max-width: 1034px) {
  #civi-marketing-timeline { width: 100%; }
  #civi-marketing-tools { width: 400px; }
}

@media (max-width: 932px) {
 #civi-marketing-role { width: 100%; }
  #civi-marketing-timeline { width: 100%; }
}


/* Differential Motor */
#differential-motor-role { width: 240px; }
#differential-motor-tools { width: 262px; }
#differential-motor-timeline { width: 228px; }

@media (max-width: 594px) {
  #differential-motor-role { width: 100%; }
}

@media (max-width: 1162px) {
  #differential-motor-tools { width: 140px; }
}

@media (max-width: 1040px) {
  #differential-motor-timeline { width: 132px; }
}

@media (max-width: 944px) {
  #differential-motor-role { width: 148px; }
}

@media (max-width: 852px) {
  #differential-motor-tools { width: 262px; }
  #differential-motor-timeline { width: 228px; }
}

@media (max-width: 839px) {
  #differential-motor-role { width: 100%; }
}

@media (max-width: 704px) {
  #differential-motor-tools { width: 262px; }
  #differential-motor-timeline { width: 228px; }
}

@media (max-width: 634px) {
  #differential-motor-tools { width: 140px; }
  #differential-motor-timeline { width: 132px; }
}


/* Enkontra */
#enkontra-role { width: 135px; }
#enkontra-tools { width: 364px; }
#enkontra-timeline { width: 255px; }

@media (max-width: 1086px) {
  #enkontra-tools { width: 242px; }
}

@media (max-width: 1064px) {
  #enkontra-timeline { width: 160px; }
}

@media (max-width: 969px) {
  #enkontra-tools { width: 364px; }
  #enkontra-timeline { width: 255px; }
}

/* Below that, on phones, none of the widths above leave enough room —
   .badge-block's own max-width: 100% safety net doesn't reliably
   catch this because the grid column it sits in sizes itself to fit
   the badge's declared width in the first place, so the fixed px
   width just wins and overflows past the header's padding on both
   sides (the column is centered, so it bleeds evenly left and right). */
@media (max-width: 640px) {
  #enkontra-tools { width: 100%; }
  #enkontra-timeline { width: 100%; }
}

/* Haus */
#haus-role { width: 134px; }
#haus-tools { width: 364px; }
#haus-timeline { width: 256px; }

@media (max-width: 1186px) {
  #haus-tools { width: 242px; }
}

@media (max-width: 1067px) {
  #haus-timeline { width: 160px; }
}

@media (max-width: 1067px) {
  #haus-role { width: 100%; }
  #haus-tools { width: 100%; }
  #haus-timeline { width: 100%; }
}

@media (max-width: 840px) {
  #haus-role { width: 134px; }
  #haus-tools { width: 364px; }
}

/* ...but that re-fixes them wider than a phone screen — same overflow
   as Enkontra above, so cap them back to 100% below that. */
@media (max-width: 640px) {
  #haus-role { width: 100%; }
  #haus-tools { width: 100%; }
}

/* Motion Graphics */
#motion-graphics-role { width: 290px; }
#motion-graphics-tools { width: 265px; }
#motion-graphics-timeline { width: 115px; }

@media (max-width: 1102px) {
  #motion-graphics-role { width: 100%; }
  #motion-graphics-tools { width: 100%; }
  #motion-graphics-timeline { width: 100%; }
}

/* Motion Graphics — Loadsmart, PortPass, and Balaroti share the same
   Role and (mostly) the same Tools; only Timeline changes per piece. */
#motion-graphics-loadsmart-role,
#motion-graphics-portpass-role,
#motion-graphics-balaroti-role,
#motion-graphics-age-role,
#motion-graphics-regenerate-pro-role,
#motion-graphics-prodiet-role { width: 390px; }

#motion-graphics-loadsmart-tools,
#motion-graphics-portpass-tools,
#motion-graphics-balaroti-tools { width: 364px; }

#motion-graphics-loadsmart-timeline { width: 165px; }
#motion-graphics-portpass-timeline { width: 260px; }
#motion-graphics-balaroti-timeline { width: 140px; }



/* Age Nutrilatina, Regenerate Pro, and Prodiet only used two Tools
   (no Lottie), so they get their own narrower width. */

#motion-graphics-age-tools,
#motion-graphics-regenerate-pro-tools,
#motion-graphics-prodiet-tools { width: 265px; }

#motion-graphics-age-timeline { width: 135px; }
#motion-graphics-regenerate-pro-timeline { width: 90px; }
#motion-graphics-prodiet-timeline { width: 135px; }

@media (max-width: 1100px) {
  #motion-graphics-loadsmart-role,
  #motion-graphics-portpass-role,
  #motion-graphics-balaroti-role,
  #motion-graphics-age-role,
  #motion-graphics-regenerate-pro-role,
  #motion-graphics-prodiet-role { width: 290px; }

  #motion-graphics-loadsmart-tools,
  #motion-graphics-portpass-tools,
  #motion-graphics-balaroti-tools { width: 264px; }

  #motion-graphics-portpass-timeline { width: 164px; }
}

@media (max-width: 911px) {
  #motion-graphics-loadsmart-role,
  #motion-graphics-portpass-role,
  #motion-graphics-balaroti-role,
  #motion-graphics-age-role,
  #motion-graphics-regenerate-pro-role,
  #motion-graphics-prodiet-role { width: 150px; }

  #motion-graphics-loadsmart-tools,
  #motion-graphics-portpass-tools,
  #motion-graphics-balaroti-tools { width: 140px; }

  #motion-graphics-portpass-timeline { width: 164px; }

  #motion-graphics-age-tools,
  #motion-graphics-regenerate-pro-tools,
  #motion-graphics-prodiet-tools { width: 142px; }
}


@media (max-width: 647px) {
  #motion-graphics-loadsmart-role,
  #motion-graphics-portpass-role,
  #motion-graphics-balaroti-role,
  #motion-graphics-age-role,
  #motion-graphics-regenerate-pro-role,
  #motion-graphics-prodiet-role,
  #motion-graphics-loadsmart-tools,
  #motion-graphics-portpass-tools,
  #motion-graphics-balaroti-tools,
  #motion-graphics-age-tools,
  #motion-graphics-regenerate-pro-tools,
  #motion-graphics-prodiet-tools,
  #motion-graphics-loadsmart-timeline,
  #motion-graphics-portpass-timeline,
  #motion-graphics-balaroti-timeline,
  #motion-graphics-age-timeline,
  #motion-graphics-regenerate-pro-timeline,
  #motion-graphics-prodiet-timeline { width: 100%; }
}

/* Omnitrack Catalogue */
#omnitrack-catalogue-role { width: 150px; }
#omnitrack-catalogue-tools { width: 260px; }
#omnitrack-catalogue-timeline { width: 250px; }

@media (max-width: 1092px) {
  #omnitrack-catalogue-tools { width: 504px; }
  #omnitrack-catalogue-timeline { width: 100%; }
}

@media (max-width: 1038px) {
  #omnitrack-catalogue-tools { width: 260px; }
}

@media (max-width: 914px) {
  #omnitrack-catalogue-timeline { width: 380px; }
}

@media (max-width: 840px) {
  #omnitrack-catalogue-timeline { width: 100%; }
}

@media (max-width: 554px) {
  #omnitrack-catalogue-tools { width: 260px; }
}

/* Plots */
#plots-role { width: 215px; }
#plots-tools { width: 100px; }
#plots-timeline { width: 340px; }

/* No breakpoints at all above, so timeline (340px) overflows a phone
   screen outright — same fix as Enkontra/Haus/Omnitrack Website. */
@media (max-width: 640px) {
  #plots-role { width: 100%; }
  #plots-tools { width: 100%; }
  #plots-timeline { width: 100%; }
}

/* SupplyCyber */
#supplycyber-role { width: 240px; }
#supplycyber-tools { width: 224px; }
#supplycyber-timeline { width: 180px; }

@media (max-width: 1097px) {
  #supplycyber-timeline { width: 100%; }
}


.badge-specs {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  margin-right: 0.5rem;
  margin-bottom: 0.5rem;
  padding: 0.4rem 0.8rem;
  background-color: var(--index);
  border-radius: 4px;
  font-size: 0.9rem;
  color: var(--ink);
  text-decoration: none;
  cursor: default;
  white-space: nowrap;
  transition: background-color 0.15s ease, transform 0.15s ease, box-shadow 0.15s ease;
  box-shadow: 0 4px 8px -4px rgba(0, 0, 0, 0.1);
}

a .badge-specs {
  cursor: pointer;
}

/* Lifts the badge slightly and deepens/softens its shadow on hover —
   a bigger blur and offset than the resting shadow reads as the pill
   floating higher off the page, not just moving up. */
a:hover .badge-specs,
a:focus-visible .badge-specs {
  background-color: #e4e2db;
  transform: translateY(-2px);
  box-shadow: 0 10px 16px -6px rgba(0, 0, 0, 0.18);
}

/* Pressed state: same idea as the copy-email button's :active — drop
   below rest and flatten the shadow, so clicking reads as physically
   pushing the pill down. */
a:active .badge-specs {
  transform: translateY(1px);
  box-shadow: 0 1px 2px -1px rgba(0, 0, 0, 0.1);
  transition-duration: 0.05s;
}

/* A badge with a hover/focus tooltip (the About page's language badges):
   same lift as a linked badge, plus a small dark card above it with the
   details. Focusable (tabindex="0" in the HTML), so keyboard users get it
   on focus and a tap on a phone shows it too. script.js nudges it to stay
   on screen near the edges. */
.badge-specs.has-tooltip {
  position: relative;
  cursor: help;
  outline: none;
}

.badge-specs.has-tooltip:hover,
.badge-specs.has-tooltip:focus-visible {
  background-color: #e4e2db;
  transform: translateY(-2px);
  box-shadow: 0 10px 16px -6px rgba(0, 0, 0, 0.18);
}

/* Languages no longer in use (German, Spanish): the badge's background and
   text are made semi-transparent, not the whole badge — opacity on the
   badge would fade its tooltip too, since the tooltip is inside it. Still
   lifts a little on hover, but stays softer than the active languages. */
.badge-specs.lang-inactive {
  background-color: rgba(238, 236, 230, 0.5);
  color: rgba(26, 26, 26, 0.55);
  box-shadow: 0 4px 8px -4px rgba(0, 0, 0, 0.05);
}

.badge-specs.lang-inactive:hover,
.badge-specs.lang-inactive:focus-visible {
  background-color: rgba(228, 226, 219, 0.75);
  color: rgba(26, 26, 26, 0.75);
  box-shadow: 0 10px 16px -6px rgba(0, 0, 0, 0.1);
}

.badge-tooltip {
  position: absolute;
  bottom: calc(100% + 10px);
  /* --tip-shift is set by script.js to nudge the card back on screen. */
  left: var(--tip-shift, 0px);
  z-index: 5;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  width: max-content;
  max-width: min(260px, 80vw);
  padding: 0.6rem 0.8rem;
  background-color: var(--ink);
  color: var(--paper);
  border-radius: 6px;
  font-size: 0.78rem;
  font-weight: 400;
  line-height: 1.45;
  text-align: left;
  white-space: normal;
  box-shadow: 0 8px 24px -8px rgba(0, 0, 0, 0.35);
  opacity: 0;
  visibility: hidden;
  transform: translateY(10px);
  pointer-events: none;
  /* A slower, silkier ease-out than the page's other fade/settle motion
     (which uses cubic-bezier(0.2, 0.8, 0.2, 1)) — decelerates more
     gently into its final position instead of gliding at a near-steady
     speed, which reads as smoother for a small element like this. */
  transition: opacity 0.4s cubic-bezier(0.16, 1, 0.3, 1), transform 0.4s cubic-bezier(0.16, 1, 0.3, 1), visibility 0s linear 0.4s;
}

.badge-tooltip strong {
  font-weight: 600;
}

/* Flag next to a language tooltip's title (English/Portuguese/Spanish
   only — see about.html). aria-hidden, since the language name in the
   text next to it already says as much; a screen reader announcing the
   emoji's own name ("flag: United Kingdom") would just be noise. */
.tooltip-flag {
  margin-right: 0.35em;
}

/* Excludes .tooltip-flag: a flag should stay full-color, not fade to
   the tooltip's muted off-white body-text color like the rest of its
   spans — it's the color rule a browser falls back to for the flag's
   own glyph if it can't render the full-color version. */
.badge-tooltip span:not(.tooltip-flag) {
  color: rgba(250, 249, 246, 0.78);
}

/* The little arrow pointing at the badge. */
.badge-tooltip::after {
  content: "";
  position: absolute;
  top: 100%;
  /* Undo the shift so the arrow still points at the badge. */
  left: calc(1.1rem - var(--tip-shift, 0px));
  border: 6px solid transparent;
  border-top-color: var(--ink);
}

.has-tooltip:hover .badge-tooltip,
.has-tooltip:focus .badge-tooltip {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
  transition-delay: 0s;
}

/* Moved below the badge by script.js when there's no room above it. */
.badge-tooltip.below {
  bottom: auto;
  top: calc(100% + 10px);
}

.badge-tooltip.below::after {
  top: auto;
  bottom: 100%;
  border-top-color: transparent;
  border-bottom-color: var(--ink);
}

/* Likewise raise the hovered/focused badge itself: its lift (a transform)
   makes it a stacking context, and the badges after it in the row paint
   over a tooltip that drops below it. */
.badge-specs.has-tooltip:hover,
.badge-specs.has-tooltip:focus {
  z-index: 40;
}

/* Raise the whole info block above its neighbours while one of its
   tooltips is open — the blocks around it are their own stacking
   contexts (they fade in with a transform) and would otherwise paint
   over a tooltip that reaches into them. */
.badge-block:hover,
.badge-block:focus-within {
  z-index: 30;
}

@media (prefers-reduced-motion: reduce) {
  .badge-tooltip {
    transition: none;
  }
}

.block-title{
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 0.5rem;
}

.block-text{
  max-width: 700px;
  font-size: 0.95rem;
  margin-bottom: 0.5rem;
}

.project-specs-4 {
  grid-template-columns: auto auto auto auto !important;
}

@media (max-width: 1300px) {
  .project-specs-4 {
    grid-template-columns: auto auto !important;
  }
  .project-specs > div {
    max-width: 45%;
    width: 100%;
  }
}

@media (max-width: 740px) {
  .project-specs-4 {
    grid-template-columns: auto !important;
  }
  .project-specs > div {
    max-width: 100%;
  }
}



.project-specs-new dt,
.project-specs dt {
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 0.35rem;
}

.project-specs-new dd,
.project-specs dd {
  font-size: 0.95rem;
}


.project-header-content{
  padding-right: 280px;
  width: 100%;
}

.project-header-content-full{
  padding-right: 0 !important;
}

@media (max-width: 840px) {
  .project-header-content{
    padding-right: 0;
  }
}

.subtitle{
  /*font-size: 0.75rem;*/
  /*font-weight: 700;*/
  /*letter-spacing: 0.06em;*/
}



/* Table of contents, auto-built by script.js from the page's H2/H3s */
.project-index {
  min-width: 210px;
  align-items: center; /* start | end | center | stretch */
  background-color: var(--index);
  border-radius: 4px;
  box-shadow: 0 8px 16px -8px rgba(0, 0, 0, 0.1);

  padding: 1.5rem;
  
  /*border-left: 1px solid rgb(228, 226, 219);
  padding-left: 2rem;*/
}

.project-index-plots {
  min-width: 210px;
}

.project-index-civi {
  min-width: 280px;
}

.project-index-label {
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 0.75rem;
}

.project-index ul {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.project-index a {
  position: relative;
  display: inline-block;
}



.project-header a {
  color: var(--link);
  text-decoration: none;
  font-size: 0.9rem;
}

.project-header a:hover,
.project-header a:focus-visible {
  color: var(--link-hover);
}

.project-hero {
  max-width: 1200px;
  margin: 0 auto 3rem;
  padding: 0 1.5rem;
}

.project-hero-static {
  max-width: 1200px;
  margin: 0 auto 3rem;
  padding: 0 1.5rem;
}

.project-hero img, .project-hero-static img {
  width: 100%;
  height: auto;
  border-radius: var(--image-radius);
  display: block;
  margin: auto;
}

.project-hero-static img {
  max-width: 700px;
}

@media (max-width: 900px) {
  /* Matches the <picture> source swap to plots-cover-mobile.webp in
     the HTML — that image is shot/cropped for this ratio. */
  .project-hero img {
    aspect-ratio: 2 / 1;
    object-fit: cover;
  }
}

@media (max-width: 700px) {
  /* Matches the <picture> source swap to plots-cover-mobile.webp in
     the HTML — that image is shot/cropped for this ratio. */
  .project-hero img {
    aspect-ratio: 4 / 5;
    object-fit: cover;
  }
}

.project-body {
  max-width: 1200px; /* 720px */
  margin: 0 auto;
  margin-top: 2rem;
  /*padding: 0 1.5rem 2rem;*/
  padding-bottom: 1rem;
}

@media (max-width: 1300px) {
  .project-body {
    padding: 0rem 1.5rem 1rem 1.5rem;
  }
}

@media (max-width: 560px) {
  .project-body {
    margin-top: 1.5rem;
  }
}

.project-body h2 {
  font-family: var(--display-font);
  font-size: 1.5rem;
  line-height: 1.15;
  margin-top: 4rem; /* 3rem; */
  margin-bottom: 1rem;
  scroll-margin-top: 6rem; /* clears the sticky site header when linked to */
}

.project-body h2:first-child {
  margin-top: 0;
}

.project-body h3 {
  font-family: var(--display-font);
  font-size: 1.05rem;
  margin-top: 2rem;
  margin-bottom: 0.75rem;
  scroll-margin-top: 6rem;
}

.project-body h4 {
  font-size: 0.8rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink);
  margin-bottom: 0.6rem;
  margin-top: 1.5rem;
}

.project-body p {
  margin-bottom: 0.5rem;
  font-size: 1.1rem;
  font-weight: 300;
  letter-spacing: 0.06em;
}

.introduction-text{
  font-size: clamp(1rem, 3vw, 1.1rem) !important;
  line-height: 1.8rem;
  padding-bottom: 1.5rem;
}
/* Forces the homepage tagline onto two lines instead of leaving it to
   wrap naturally — below 460px the typewriter's changing word length
   pushed it to 3 lines, with just one or two words stranded alone on
   the last line. Hidden by default (display: none) so it does nothing
   above that width, where natural wrapping already looks fine. */
.tagline-break {
  display: none;
}

@media (max-width: 994px) {
  .tagline-break {
    display: block;
  }
}

@media (max-width: 650px) {
  .tagline-break {
    display: none;
  }
}

@media (max-width: 460px) {
  .tagline-break {
    display: block;
  }
}

.project-body ul {
  margin: 0 0 1rem 1.25rem;
}

.project-body li {
  margin-bottom: 0.5rem;
  font-size: 1.1rem;
  font-weight: 300;
  letter-spacing: 0.06em;
}

/* Case-study screenshots: image + caption, full width of the text column */
.project-figure {
  margin: 2rem 0;
}

.project-figure img {
  width: 100%;
  height: auto;
  border-radius: var(--image-radius);
  display: block;
  background-color: var(--paper); /* #ece9e2*/
}

.project-figure figcaption {
  margin-top: 0.8rem;
  font-size: 0.9rem;
  font-weight: 400;
  line-height: 1.5;
  letter-spacing: normal;
  color: var(--muted);
  text-align: center;
}

/* A single row of images sharing one fixed height, each keeping its own
   aspect ratio (so width varies naturally) instead of the equal-width
   columns .figure-grid uses. Wraps to multiple lines on narrow screens
   rather than shrinking every image down to fit. */
.figure-row {
  display: grid;
  grid-template-columns: auto auto auto auto auto auto;
  flex-wrap: wrap;
  align-items: center;
  gap: 2rem auto;
  background-color: #ffffff;
  padding: 2rem;
  margin-bottom: 2rem;
}



.figure-row .project-figure {
  margin: 0;
}

.figure-row .project-figure img {
  width: auto;
  height: 200px;
}

@media (max-width: 1120px) {
  .figure-row .project-figure img {
    height: 150px;
  }
  
}

@media (max-width: 860px) {
  .figure-row .project-figure img {
    height: 120px;
  }
  
}

@media (max-width: 700px) {
  .figure-row {
    grid-template-columns: auto auto;
  }

  .figure-row .project-figure img {
    height: 240px;
  }
}

@media (max-width: 620px) {
  .figure-row {
    grid-template-columns: auto auto;
    gap: 4rem auto;
  }

  .figure-row .project-figure img {
    height: 180px;
  }
}

/* Related screenshots shown side by side instead of one long stack */
.figure-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1.5rem;
  margin: 2rem 0;
}

.figure-grid .project-figure {
  margin: 0;
}

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

.figure-grid-4 {
  /* minmax(0, 1fr), not 1fr, so the four columns can shrink to fit a phone
     instead of running past its edge. */
  grid-template-columns: repeat(4, minmax(0, 1fr));
}


/* Carousel: alternates between however many .carousel-slide elements
   are inside it. script.js builds the dots and wires up prev/next —
   this just handles the visuals. */
.project-carousel {
  position: relative;
  margin: 2rem 0;
}

.carousel-viewport {
  position: relative;
  overflow: hidden;
  border-radius: var(--image-radius);
  background-color: var(--carousel-bg);
  aspect-ratio: 3 / 2;
  /* Let JS own horizontal swipe; the browser still handles vertical
     page scroll, so the two gestures don't fight each other. */
  touch-action: pan-y;
  cursor: grab;
  max-height: 600px;
  margin-left: auto;
  margin-right: auto;
}

.carousel-bg-plots {
  background-color: var(--carousel-bg-plots);
}
/*
.carousel-bg-civi {
  background-color: var(--carousel-bg-civi);
}

.carousel-bg-omnitrack {
  background-color: var(--carousel-bg-omnitrack);
}*/

.carousel-bg-white {
  background-color: var(--carousel-bg-white);
}

.carousel-bg-grey {
  background-color: var(--carousel-bg-grey);
}

.carousel-viewport:active {
  cursor: grabbing;
}

.carousel-track {
  display: flex;
  height: 100%;
  /* A slower ease-out curve than the plain "ease" this used to have —
     quick to start, then a long, gentle deceleration into the next
     slide (the same expo-out shape as the Lenis smooth-scroll easing
     in script.js) instead of settling abruptly. */
  transition: transform 0.85s cubic-bezier(0.16, 1, 0.3, 1);
}

@media (prefers-reduced-motion: reduce) {
  .carousel-track {
    transition: none;
  }
}

.carousel-slide {
  flex: 0 0 100%;
  min-width: 0;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
}

.carousel-slide img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  cursor: zoom-in;
}

/* Visual style (size, background, hover/active) lives on the shared
   .lightbox-btn, .carousel-prev, .carousel-next rule near the lightbox
   styles — this is just positioning within the carousel. */
.carousel-prev,
.carousel-next {
  position: absolute;
  /* Positioned relative to .carousel-viewport (see above), not
     .project-carousel — that wrapper also contains the caption and
     dots below the image, so top:50% there landed below the image's
     actual center. Now this 50% is against just the image area. */
  top: 50%;
  transform: translateY(-50%);
}

.carousel-prev:active,
.carousel-next:active {
  transform: translateY(-50%) scale(0.9);
}

.carousel-prev {
  left: 0.75rem;
}

.carousel-next {
  right: 0.75rem;
}

@media (max-width: 640px) {
  /* Swipe (already touch-only — see script.js) is the only way to
     navigate the carousel on mobile; the buttons are just clutter
     over the image at that size. */
  .carousel-prev,
  .carousel-next {
    display: none;
  }
}

.project-carousel .carousel-caption {
  margin-top: 0.8rem;
  font-size: 0.9rem;
  font-weight: 400;
  line-height: 1.5;
  letter-spacing: normal;
  color: var(--muted);
  text-align: center;
  /* Crossfade target for the opacity toggle in script.js's show() —
     the text itself swaps instantly (there's no way to animate that
     part), but fading out/in around the swap reads as one smooth
     change instead of a hard cut. Matches the 350ms swap delay in
     show(), which waits for this fade-out to finish before swapping
     the text and fading back in. */
  transition: opacity 0.35s ease-in-out;
}

.carousel-dots {
  display: flex;
  justify-content: center;
  gap: 0.5rem;
  margin-top: 0.75rem;
}

.carousel-dot {
  width: 8px;
  height: 8px;
  /* var(--image-radius) (4px) would round this into a circle again at
     this size — needs a proportionally smaller radius to actually
     read as a tiny square instead. */
  border-radius: 2px;
  border: none;
  padding: 0;
  background-color: #d8d5cc;
  cursor: pointer;
  transition: background-color 0.4s ease-in-out, transform 0.4s ease-in-out;
}

/* ---- TEST: alternate carousel controls layout, buttons below the
   image instead of overlaid on top of it, flanking the caption/dots.
   Scoped with :has() to carousels that actually have a
   .carousel-controls-row, so every other .project-carousel on the
   site (still using the default overlay buttons) is untouched — no
   HTML changes needed anywhere except the carousels being tried out. */
.project-carousel:has(.carousel-controls-row) .carousel-prev,
.project-carousel:has(.carousel-controls-row) .carousel-next {
  /* relative, not static — it still needs to be a positioned element
     so its .hover-stroke-overlay child (position: absolute; inset:
     5px, from setUpHoverStroke in script.js) stays contained inside
     the button's own 44px box instead of escaping to size itself
     against the next positioned ancestor up (.project-carousel). */
  position: relative;
  /* The default overlay rule's left: 0.75rem / right: 0.75rem (meant
     to inset the button from the image's edge when it was absolutely
     positioned) still apply to a relatively positioned element too —
     left/right only go inert under position: static. Without this
     reset they nudge the buttons 12px in from the row's edges instead
     of leaving them flush with it. */
  left: auto;
  right: auto;
  flex-shrink: 0;
  /* Without this, the default overlay rule's transform:
     translateY(-50%) (meant to center the button against the image
     when it was absolutely positioned) keeps applying here too — it
     doesn't require position: absolute to take effect, so it was
     quietly shifting the button up by half its own height at rest,
     making the hover/active states below (which replace this value,
     not add to it) look like they move the wrong way. */
  transform: none;
  /* Nudges the buttons down from the row's centered position so they
     line up with the caption/dots text instead of sitting above it. */
  margin-top: 8px;
  /* Same hover/active treatment as the homepage social-bar buttons
     (e.g. the mail icon), but fully transparent at rest rather than
     their faint paper tint — these buttons sit lower, over the page's
     dotted background and sometimes over a carousel's own drop
     shadow, where any visible fill at rest reads as a stray box/edge
     instead of a bare icon. */
  background-color: transparent;
  box-shadow: none;
  /* The default overlay button's frosted-glass blur still applies
     here otherwise — transparent background-color alone doesn't
     remove it, since backdrop-filter blurs/saturates whatever's
     behind the button regardless of its own fill. */
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  transition: color 0.2s ease, background-color 0.2s ease, transform 0.15s ease, box-shadow 0.15s ease;
}

.project-carousel:has(.carousel-controls-row) .carousel-prev:hover,
.project-carousel:has(.carousel-controls-row) .carousel-next:hover {
  background-color: var(--link-hover);
  color: #fff;
  transform: translateY(-2px);
  box-shadow: 0 10px 16px -6px rgba(0, 0, 0, 0.18);
}

.project-carousel:has(.carousel-controls-row) .carousel-prev:active,
.project-carousel:has(.carousel-controls-row) .carousel-next:active {
  transform: translateY(1px);
  box-shadow: 0 1px 2px -1px rgba(0, 0, 0, 0.1);
  transition-duration: 0.05s;
}

.carousel-controls-row {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 1rem;
  margin-top: 0.8rem;
}

/* Matches .carousel-frame-phone's own max-width so the buttons land
   next to the narrower phone mockup instead of spanning the full,
   much wider column. */
.carousel-frame-phone + .carousel-controls-row {
  max-width: 260px;
  margin-left: auto;
  margin-right: auto;
}

/* For carousels whose image is narrower than the column: the default
   3:2 viewport is capped at 600px tall (see .carousel-viewport), so it is
   at most 900px wide and centered. This keeps the buttons flush with the
   image's left and right edges instead of the wider column. */
.carousel-controls-row-image-width {
  max-width: 900px;
  margin-left: auto;
  margin-right: auto;
}

.carousel-meta {
  flex: 1;
  min-width: 0;
}

.carousel-controls-row .carousel-caption {
  margin-top: 0;
}

.carousel-controls-row .carousel-dots {
  margin-top: 0.4rem;
}

@media (max-width: 640px) {
  /* These buttons sit below the image now, not over it, so the
     rationale for hiding them on mobile (touch swipe is enough,
     buttons are just clutter over the picture) no longer applies. */
  .project-carousel:has(.carousel-controls-row) .carousel-prev,
  .project-carousel:has(.carousel-controls-row) .carousel-next {
    display: flex;
  }
}

/* Discrete "browser window" frame — just a thin title bar with three
   dots, no address bar or other chrome, so it reads as a computer
   screen without competing with the screenshots inside it. Sits
   inside .carousel-frame (see below), which is what the prev/next
   buttons actually position against — .browser-mockup itself needs
   overflow: hidden to clip its own rounded corners, which would also
   clip the buttons if they lived in here too. */
.browser-mockup {
  border: 2px solid transparent;
  border-radius: var(--image-radius);
  overflow: hidden;
  box-shadow: 0 20px 40px rgba(0, 0, 0, 0.18), 0 4px 10px rgba(0, 0, 0, 0.1);
  /* Chrome-bezel effect: a light/dark banded gradient standing in for
     the border, clipped to border-box, with a flat inner fill (clipped
     to padding-box) sitting on top so it reads as a thin metal edge
     rather than a gradient fill behind the content. */
  background-image: linear-gradient(#fff, #fff),
    linear-gradient(160deg, #d4d4d4 0%, #d4d4d4 15%, #b8b8b8 32%, #d4d4d4 48%, #b0b0b0 62%, #d4d4d4 80%, #d4d4d4 100%);
  background-origin: border-box;
  background-clip: padding-box, border-box;
}

.browser-mockup .carousel-viewport {
  border-radius: 0;
}

/* Wraps just .browser-mockup (not the caption/dots below it), so its
   rendered height matches the frame exactly — that's what lets
   top: 50% below center the buttons on the frame itself rather than
   on the taller .project-carousel (frame + caption + dots). No
   overflow: hidden here, so the buttons (positioned outside the
   frame's edges) aren't clipped the way they would be inside
   .browser-mockup. */
.carousel-frame {
  position: relative;
}

.carousel-frame .carousel-prev {
  left: -1.5rem;
}

.carousel-frame .carousel-next {
  right: -1.5rem;
}

/* The phone mockup renders narrower than its column (max-width: 260px,
   centered), so matching the frame's own width to it keeps the prev/next
   buttons close to the phone itself instead of the wider column edges. */
.carousel-frame-phone {
  max-width: 260px;
  margin: 0 auto;
}

.carousel-frame-phone .carousel-prev {
  left: -60px;
}

.carousel-frame-phone .carousel-next {
  right: -60px;
}

.browser-mockup-bar {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 0.55rem 0.75rem;
  background-color: var(--index);
}

.browser-mockup-bar span {
  display: inline-block;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background-color: rgba(0, 0, 0, 0.15);
}

/* Same discrete idea as .browser-mockup — a thin bar, no other chrome
   — but narrow and more rounded to read as a phone instead of a
   laptop/browser window. The bar's "notch" pill stands in for the
   dots above. Goes inside the same .carousel-frame as the desktop
   version, so it gets the same outside-the-frame prev/next arrows. */
.phone-mockup {
  border: 2px solid transparent;
  border-radius: 28px;
  overflow: hidden;
  max-width: 260px;
  margin: 0 auto;
  box-shadow: 0 20px 40px rgba(0, 0, 0, 0.18), 0 4px 10px rgba(0, 0, 0, 0.1);
  background-image: linear-gradient(#fff, #fff),
    linear-gradient(160deg, #d4d4d4 0%, #d4d4d4 15%, #b8b8b8 32%, #d4d4d4 48%, #b0b0b0 62%, #d4d4d4 80%, #d4d4d4 100%);
  background-origin: border-box;
  background-clip: padding-box, border-box;
}

.phone-mockup .carousel-viewport {
  border-radius: 0;
}

.phone-mockup-bar {
  display: flex;
  justify-content: center;
  padding: 0.6rem 0;
  background-color: var(--index);
}

.phone-mockup-bar span {
  width: 50px;
  height: 5px;
  border-radius: 3px;
  background-color: rgba(0, 0, 0, 0.15);
}

/* Fixed-height, vertically-scrollable variant for full-page website
   screenshots — instead of shrinking each one down to fit a fixed
   aspect-ratio box, this lets the actual screenshot scroll inside a
   window-sized frame. The scrolling happens per-slide (see below),
   not on the viewport itself — each screenshot is a different
   length, so a single shared scroll area sized to the tallest one
   would let shorter slides scroll past their own bottom edge into
   blank space. overflow-x stays hidden so the horizontal slide-swipe
   still works. */
.carousel-viewport-scroll {
  aspect-ratio: unset;
  height: 480px;
  overflow: hidden;
  cursor: default;
}

.carousel-viewport-scroll .carousel-track {
  height: 100%;
  align-items: flex-start;
}

.carousel-viewport-scroll .carousel-slide {
  height: 100%;
  flex-direction: column;
  align-items: flex-start;
  /* Without this, the inherited justify-content: center clips the
     start of overflowing content (flexbox centers before allowing
     scroll) — flex-start keeps the very top scrollable into view. */
  justify-content: flex-start;
  overflow-y: auto;
  overflow-x: hidden;
}

.carousel-viewport-scroll .carousel-slide img {
  height: auto;
  object-fit: contain;
  cursor: zoom-in;
}

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

.carousel-dot.active:hover {
  background-color: var(--link-hover);
}

/* Excludes the active dot — it's already bigger and its own color, so a
   hover on it shouldn't compete with that. */
.carousel-dot:not(.active):hover,
.carousel-dot:not(.active):focus-visible {
  background-color: #a8a498;
  transform: scale(1.2);
}

@media (prefers-reduced-motion: reduce) {
  .carousel-dot,
  .project-carousel .carousel-caption {
    transition: none;
  }
}

.topic-divider{
  border-bottom: 1px solid #e4e2db;
  padding-top: 3rem;
  padding-bottom: 2.5rem;
  scroll-margin-top: calc(4rem - 20px); /* tucks the previous divider's border (the line just above this section) fully under the docked header (~3.3rem), then padding-top below gives the h2 breathing room */
}

.topic-divider-index{
  border-bottom: 1px solid #e4e2db;
  padding-bottom: 1.5rem;
  scroll-margin-top: 4rem; /* tucks the previous divider's border under the docked header (~3.3rem), then padding-top below gives the h2 breathing room */
}

.topic-divider-no-line{
  border-bottom: 0 !important;
  padding-top: 3rem !important;
}

/* The divider lines — under each section (.topic-divider) and under the
   title block at the top of a page (.divider) — fade in as they scroll
   into view and when the page opens. A border can't be
   faded on its own, so script.js marks each divider .has-divider-line,
   makes its 1px border transparent (it still takes up its 1px, so no
   layout shifts) and draws the line as a separate .divider-line element
   in exactly that spot, whose opacity it toggles. Without JavaScript none
   of this applies and the plain border stays. */
.topic-divider.has-divider-line,
.topic-divider-index.has-divider-line,
.divider.has-divider-line {
  position: relative;
  border-bottom-color: transparent;
}

.divider-line {
  position: absolute;
  left: 0;
  right: 0;
  bottom: -1px;
  height: 1px;
  background-color: #e4e2db;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.9s ease;
}

.divider-line.in-view {
  opacity: 1;
}

/* First paint of pages that opt in to the opening animation (the
   homepage, About and project pages set .js-reveal in their <head>): hide
   the borders right away so a line doesn't flash before script.js takes
   over and fades it in. */
@media (prefers-reduced-motion: no-preference) {
  html.js-reveal .topic-divider,
  html.js-reveal .topic-divider-index,
  html.js-reveal .divider {
    border-bottom-color: transparent;
  }
}

.topic-divider-first{
  padding-top: 0.5rem;
}

@media (max-width: 900px) {
  .topic-divider{
    scroll-margin-top: calc(3.25rem - 20px);
  }

  .topic-divider-first{
    padding-top: 0rem;
  }
}

@media (max-width: 600px) {
  .topic-divider{
    scroll-margin-top: calc(2.5rem - 20px);
  }

  .topic-divider-first{
    padding-top: 0rem;
  }
}

/* Project header: stats (3fr) next to the "On this page" index (1fr).
   Below 900px there isn't room for both, so the index drops under the
   stats and both go full width. */
.project-header-columns {
  display: grid;
  align-items: start;
  gap: 0;
  margin-top: 2rem;
  padding: 0;
  justify-content: center;
}

.project-header-columns-full {
  padding-right: 0 !important;
}

@media (max-width: 840px) {
  .project-header-columns {
    grid-template-columns: 1fr;
  }
  

  .project-index {
    max-width: 250px;
    min-width: 210px;
    background-color: var(--index);
    border-radius: 4px;
    box-shadow: 0 8px 16px -8px rgba(0, 0, 0, 0.1);
    padding: 1.5rem;
  }

  .project-index ul {
    gap: 0.3rem;
  }

  .project-index-label {
    margin-bottom: 0.35rem;
  }
}

/* Desktop: the index floats to the right of the specs instead of
   sharing a grid track with them — reserve room on the right for it
   (padding-right) and pin it there (position: absolute, relative to
   this container so it lines up with the top of the specs row
   regardless of how tall the H1/subtitle above happen to be). It's
   positioned only — it scrolls away normally with the rest of the
   header, it doesn't stay pinned as you scroll the page. */
@media (min-width: 840px) {
  .project-header-columns {
    position: relative;
    grid-template-columns: 1fr;
    padding-right: 240px;
  }

  /* Both columns share the single grid cell instead of stacking into
     two rows — the second one (wrapping the now absolutely-positioned
     .project-index) is otherwise empty, but a grid row still gets
     created for it, and gap still adds space above that empty row
     even though nothing visible is there to see. */
  .project-header-columns > .split-column {
    grid-row: 1;
    grid-column: 1;
  }

  .project-index {
    position: absolute;
    top: -200px;
    right: 0;
    width: 260px;
  }
}

@media (max-width: 1200px) {
  .project-index {
    right: 0;
  }
}


/* Two-column layout: put images in one .split-column and text in the
   other. Order in the HTML decides left vs. right — put the images
   markup first for image-left/text-right, or swap it for the reverse.
   items: start keeps each column's own height (the taller one doesn't
   stretch the other), and it collapses to a single stacked column on
   narrow screens. */
.split-columns, .split-columns-tool-kit {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 3rem;
  align-items: start;
  margin: 1rem 0 1rem 0;
}

.split-column .project-figure:first-child {
  margin-top: 0;
  
}


@media (max-width: 856px) {
  .split-columns-tool-kit {
    grid-template-columns: 1fr;
    gap: 0rem;
  }
}

@media (max-width: 700px) {
  .split-columns {
    grid-template-columns: 1fr;
    gap: 1.5rem;
  }
}



.grid {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr 1fr;
  gap: 3rem;
  align-items: start;
  margin: 2rem 0;
}

@media (max-width: 900px) {
  .grid {
    grid-template-columns: 1fr 1fr;
    gap: 1.5rem 3rem;
  }
}

@media (max-width: 600px) {
  .grid {
    grid-template-columns: 1fr;
    gap: 0.5rem;
  }
}




/* Text next to a large image, in a fixed 1:3 ratio (text:image) once
   there's room for both at their minimum widths. Below 1200px wide —
   or whenever 280px + 700px + the gap won't fit — the media query
   drops it to a single column, image below text, both full width.
   Reuses .split-column for the two children, same as .split-columns. */
.text-image-columns {
  display: grid;
  grid-template-columns: minmax(300px, 1fr) minmax(auto, 2fr);
  gap: 3rem;
  align-items: center;
}

.two-columns {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 3rem;
  align-items: center;
  /*margin: 2rem 0;*/
}

@media (max-width: 1024px) {
  .text-image-columns {
    /* minmax(0, 1fr), not 1fr: a plain 1fr can't shrink below its
       content's minimum width, which pushed the column past the screen. */
    grid-template-columns: minmax(0, 1fr);
    gap: 1.5rem;
  }
}

@media (max-width: 700px) {
  .two-columns {
    grid-template-columns: 1fr;
    gap: 1.5rem;
  }
}

/* Figma embed: always keeps a 16:9 frame — width fills its container,
   height is derived from that width rather than a fixed pixel value,
   so it stays proportional (and doesn't overflow) at any screen size. */
.figma-imbeded {
  display: block;
  width: 100%;
  aspect-ratio: 3 / 1;
  height: auto;
  border: 1px solid rgba(0, 0, 0, 0.1);
  border-radius: var(--image-radius);
}

.figma-imbeded-div {
  position: relative;
  background-color: #ffffff;
  border-radius: var(--image-radius);
  overflow: hidden;
  margin-bottom: 1rem;
}

.figma-loading-spinner {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 28px;
  height: 28px;
  margin: -14px 0 0 -14px;
  border: 3px solid rgba(0, 0, 0, 0.1);
  border-top-color: rgba(0, 0, 0, 0.35);
  border-radius: 50%;
  animation: figma-spin 0.8s linear infinite;
  pointer-events: none;
}

@keyframes figma-spin {
  to {
    transform: rotate(360deg);
  }
}

/* Phones: no Figma embeds (they crash mobile browsers), just a link.
   "Phone" = a narrow screen, or a touch-only device up to a little over
   the size of a phone held sideways. The sentence above each embed
   (.figma-note, built in script.js) swaps to its one-link version, and
   the embed itself is hidden — script.js's lazy loader never gives a
   hidden iframe its src, so it isn't downloaded either. */
/* Two-class selectors: .inline-link (declared further down) also sets
   display, and would otherwise win the tie and keep this link visible. */
.figma-note .figma-note-mobile {
  display: none;
}

@media (max-width: 900px), (hover: none) and (pointer: coarse) and (max-width: 1100px) {
  .figma-note .figma-note-desktop {
    display: none;
  }

  .figma-note .figma-note-mobile {
    display: inline-block;
  }

  .figma-imbeded-div {
    display: none !important;
  }
}

@media (max-width: 700px) {
  .figma-imbeded {
    aspect-ratio: 1 / 1;
  }
}

/* Same idea as .figma-imbeded but for YouTube demo videos, which are
   natively widescreen rather than a wide design-file canvas. */
.video-embed {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9;
  height: auto;
  border: 1px solid rgba(0, 0, 0, 0.1);
  margin-top: 2rem;
  margin-bottom: 2rem;
}

/* A self-hosted, autoplaying, silent loop used like an animated GIF —
   no player chrome, no controls, just a short demo of an interaction.
   Unlike .video-embed (a real iframe player), this is a plain <video>,
   so it gets the same rounded-corner treatment as project imagery. */

.video{
  display: block;
  object-fit: cover;
}

.inline-video {
  width: 100%;
  max-width: 700px;
  height: auto;
  /*aspect-ratio: 5 / 3;*/
  background-color: var(--paper);
  margin-bottom: 1.5rem;
}

.video-carousel {
  width: 100%;
  height: auto;
  /*aspect-ratio: 5 / 3;*/
  background-color: var(--paper);
}

.project-nav {
  max-width: 1200px; /* 720px */
  margin: 0 auto;
  display: flex;
  justify-content: space-between;
  /*adding: 1.5rem 1.5rem;*/
  padding-top: 1.5rem;
}

.project-nav-top{
  padding-top: 0 !important;
  padding-bottom: 1rem !important;
}

/* A lone link that should sit on the right (the homepage's "About me →");
   with a single child, space-between would otherwise leave it on the left. */
.project-nav-end {
  justify-content: flex-end;
}

.project-nav a {
  position: relative;
  display: inline-block;
  color: var(--muted);
  text-decoration: none;
  font-size: 0.9rem;
  transition: color 0.2s ease;
}

/* The dark link blue, same as the top-menu and other link hovers, with
   the underline sweep in that blue too. */
.project-nav a:hover,
.project-nav a:focus-visible {
  color: var(--link-hover);
}

.project-nav a .underline-hover-overlay {
  color: var(--link-hover);
}

/* ============================================
   9. ABOUT PAGE — experience timeline
   ============================================ */

/* Featured roles: date column + content column, generous spacing —
   used for the handful of positions worth a real description. */
.timeline {
  display: flex;
  flex-direction: column;
  gap: 3rem;
  margin-bottom: 1rem;
}

/* Lines up "Product Design" / "Graphic design" / "Technical roots"
   with the content column of the entries below them (175px date
   column + the 2rem gap after it), instead of sitting flush left with
   the date badges. Reset on mobile, where the grid collapses to a
   single column and the content itself no longer starts at that
   x-position. */
.timeline-group h3 {
  /*margin-left: calc(175px + 2rem);*/
}

.timeline-group p {
  font-size: clamp(0.9rem, 2vw, 1rem) !important;
}

@media (max-width: 640px) {
  .timeline-group h3 {
    display: none;
  }
}

.timeline-entry {
  display: grid;
  grid-template-columns: 175px 1fr;
  gap: 0.5rem 2rem;
  position: relative;
}

/* Wraps one or more .timeline blocks that should read as a single
   continuous timeline — Experience wraps its "Product Design" /
   "Graphic design" / "Technical roots" blocks together so the rail runs
   through all three (and their h3s) instead of restarting at each one;
   Education wraps its lone block the same way for consistency. Keeping
   the rail on this shared wrapper rather than on each individual block
   is what makes that continuity possible — the dots stay positioned
   per-entry either way. */
.timeline-group {
  position: relative;
  max-width: 900px;
  margin-left: auto;
  margin-right: auto;
}

/* The vertical "rail" running down the timeline, and a dot on it for
   each entry marking where that entry's date badge connects to it.
   Positioned at 200/202px — the date column's width — on both the
   line (relative to .timeline-group, which spans every block inside
   it) and the dot (relative to its own entry, which starts flush with
   that same left edge), so the two line up exactly. Hidden on narrow
   screens, where the date column drops above the content instead of
   sitting beside it, so there's no fixed x-position for either to
   align to anymore. */

/* TIMELINE LINE */
.timeline-group::before {
  content: '';
  position: absolute;
  left: 230px;
  width: 1px;
  background-color: #e4e2db;
}

.timeline-experience::before {
  top: 5.5rem;
  bottom: 5rem;
}

.timeline-education::before {
  top: 3rem;
  bottom: 8rem;
}

@media (max-width: 700px) {
  .timeline-group::before {
    bottom: 6rem;
  }
}

.timeline-start::before {
  content: '';
  position: absolute;
  width: 3px;
  height:20px;
  left: 22px;
  top: -20px;
  background-color: var(--paper);
  /*background-color: #ff0000;*/
}

.timeline-end::after {
  content: '';
  position: absolute;
  left: 22px;
  top:50px;
  width: 3px;
  background-color: var(--paper);
  /*background-color: #ff0000;*/
  height:110px;
  /*display: none;*/
}

/*@media (max-width: 640px) {
  .timeline-end::after {
    height:200px;
  }
}*/





/* TIMELINE BADGE */
.timeline-date {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 0.5rem;
  padding-top: 0.15rem;
}

/* TIME LINE PIN*/
.timeline-entry::before {
  content: '';
  position: absolute;
  left: 202px;
  top: 2rem;
  width: 5px;
  height: 5px;
  margin-left: -4px;
  border-radius: 50%;
}

/* Reuses the same pill look as the project pages' Role/Tool/Timeline
   badges, but the date column here is a fixed 200px — narrower than
   badges normally sit in — so text needs to wrap inside the pill
   instead of the badge's usual white-space: nowrap, which would just
   overflow past the column for longer entries like "3 years,
   part-time". align-items: stretch above makes every badge fill the
   column's full width, so its right edge lands right at the rail —
   needed for the first badge's tooltip-style pointer just below to
   actually touch the line instead of floating short of it. */
.timeline-date .badge {
  white-space: normal;
  text-align: left;
  cursor: default;
}

/* Tooltip-style pointer on the first (date-range) badge only, aimed
   at the rail — the other badges in the same column (duration, etc.)
   are just plain stacked labels underneath it. */
.timeline-date .badge:first-child {
  position: relative;
}

/* TRIANGLE BADGE */
.timeline-date .badge:first-child::after {
  content: '';
  position: absolute;
  top: 50%;
  right: -6px;
  transform: translateY(-50%);
  width: 0;
  height: 0;
  border-top: 5px solid transparent;
  border-bottom: 5px solid transparent;
  border-left: 6px solid var(--index);
}

@media (max-width: 640px) {
  .timeline-group::before {
    left: 24px;
  }

  /* TIMELINE BADGE */
  .timeline-date {
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
    padding-left: 60px;
    padding-bottom: 0.5rem;
  }

  .timeline-date .badge {
    font-size: 0.8rem;
  }

  .timeline-date .badge:first-child::after {
    display:none;
  }
}





.timeline-role {
  font-weight: 700;
}

.timeline-company {
  color: var(--muted);
  font-size: 0.9rem;
  margin-bottom: 0.5rem;
}

.timeline-title-row {
  display: flex;
  align-items: center;
  gap: 0.75rem;
}

.timeline-logo-link {
  display: block;
  flex-shrink: 0;
  line-height: 0;
  border-radius: 9px;
  transition: opacity 1s ease;
}

.timeline-logo-link:hover {
  opacity: 0.8;
}

.timeline-logo {
  width: 49px;
  height: 49px;
  border-radius: 4px;
  object-fit: cover;
  flex-shrink: 0;
}

.timeline-entry p {
  max-width: 640px;
  margin-left: calc(49px + 0.75rem);
}

.timeline-link {
  position: relative;
  display: inline-block;
  margin-left: calc(49px + 0.75rem);
  color: var(--muted);
  text-decoration: none;
}

.timeline-link:hover,
.timeline-link:focus-visible {
  color: var(--link-hover);
}

.link {
  display: inline-block;
  font-size: 0.85rem;
}

.link a {
  position: relative;
  display: inline-block;
  color: var(--muted);
  text-decoration: none;
}

.link a:hover,
.link a:focus-visible {
  color: var(--link-hover);
}

/* Generic reusable version of the same pattern for a link sitting
   inline within a larger sentence (rather than its own standalone
   "<p class="link">"), e.g. the "Get in touch" paragraph's LinkedIn
   link — was previously unstyled, relying on the browser's default
   blue + underline. */
.inline-link {
  position: relative;
  display: inline-block;
  color: var(--link);
  text-decoration: none;
}

.inline-link:hover,
.inline-link:focus-visible {
  color: var(--link-hover);
}

@media (max-width: 640px) {
  .timeline-entry {
    grid-template-columns: 1fr;
    gap: 0.15rem;
  }

  .timeline-date {
    padding-top: 0;
  }
}

.about-portrait-row {
  display: flex;
  align-items: center;
  gap: 2rem;
}

.about-portrait {
  width: 140px;
  height: 140px;
  border-radius: var(--image-radius);
  object-fit: cover;
  flex-shrink: 0;
}

@media (max-width: 500px) {
  .about-portrait-row {
    flex-direction: column;
    align-items: flex-start;
    gap: 1.25rem;
  }

  .about-portrait {
    width: 110px;
    height: 110px;
  }
}

/* ============================================
   10. COPY-EMAIL BUTTON
   ============================================ */
.copy-email-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  vertical-align: middle;
  width: 1.6rem;
  height: 1.6rem;
  padding: 0;
  margin-left: 0px;
  border: none;
  background: none;
  color: var(--muted);
  cursor: pointer;
  border-radius: 5px;
  transition: color 0.15s ease, background-color 0.15s ease, transform 0.15s ease, box-shadow 0.15s ease;
}

/* Dark blue icon, a touch bigger — scale rather than a font-size change,
   so growing it doesn't reflow the text it sits inline with. */
.copy-email-btn:hover {
  color: var(--link-hover);
  transform: scale(1.15);
}

.copy-email-btn:active {
  transform: translateY(1px);
  box-shadow: 0 1px 2px -1px rgba(0, 0, 0, 0.1);
  transition-duration: 0.05s;
}

.copy-email-btn .material-symbols-outlined {
  font-size: 1.05rem;
}

.copy-email-btn.copied {
  color: #1d5e29 !important;
}

/* Small qualifier next to a toolkit category heading (e.g. "(basic)"
   on Development & data) — flags a lower confidence level without
   needing its own heading or paragraph. */
.toolkit-category-note {
  font-size: 0.75rem;
  font-weight: 400;
  color: var(--muted);
}

/* Toolkit badges — icon + name pills, sitting under the Skills & tools
   grid. Reuses the same muted paper tone as the "On this page" index
   box, just in pill form. */
.badge-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
  margin-top: 0.7rem;
  margin-bottom: 2.2rem;
}

.graphic-motion{ max-width: 400px; }
.collaboration-documentation{ max-width: 300px; }

@media (max-width: 1159px) {
  .development-data{ max-width: 480px; }
}

@media (max-width: 1008px) {
  .development-data{ max-width: 340px; }
}

@media (max-width: 1056px) {
  .research-testing{ max-width: 300px; }
}

.badge {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.4rem 0.8rem;
  background-color: var(--index);
  border-radius: 4px;
  font-size: 0.9rem;
  color: var(--ink);
  text-decoration: none;
  cursor: default;
  white-space: nowrap;
  transition: background-color 0.15s ease, transform 0.15s ease, box-shadow 0.15s ease;
  box-shadow: 0 4px 8px -4px rgba(0, 0, 0, 0.1);
}

a.badge {
  color: var(--ink);
  cursor: pointer;
}

/* Same lift-and-deepen-shadow hover as .badge-specs above. */
a.badge:hover,
a.badge:focus-visible {
  background-color: #e4e2db;
  transform: translateY(-2px);
  box-shadow: 0 10px 16px -6px rgba(0, 0, 0, 0.18);
}

/* Same pressed state as .badge-specs above. */
a.badge:active {
  transform: translateY(1px);
  box-shadow: 0 1px 2px -1px rgba(0, 0, 0, 0.1);
  transition-duration: 0.05s;
}

.badge-icon {
  width: 1.1rem;
  height: 1.1rem;
  flex-shrink: 0;
}

/* ============================================
   11. CONTACT SECTION — social icon row
   ============================================ */
.social-links {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 4rem;
}

.social-links a {
  color: var(--ink);
  text-decoration: none;
}

.social-links .copy-email-btn {
  color: var(--ink);
}

.social-links a,
.social-links .copy-email-btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: auto;
  height: auto;
  /* Same frosted-glass treatment as the carousel arrows/lightbox
     controls/back-to-top, but a bit bigger and more transparent at
     rest — this row reads better with a lighter touch than the
     buttons floating over photos do. Negative margin cancels the
     padding's effect on the row's own gap, so the icon-to-icon
     spacing stays what it was before this box existed. */
  padding: 16px;
  margin: -16px;
  background-color: rgba(250, 249, 246, 0.15);
  backdrop-filter: blur(8px) saturate(180%);
  -webkit-backdrop-filter: blur(8px) saturate(180%);
  border: none;
  border-radius: var(--image-radius);
  cursor: pointer;
  transition: color 0.2s ease, background-color 0.2s ease, transform 0.15s ease, box-shadow 0.15s ease;
}

@supports not (backdrop-filter: blur(1px)) {
  .social-links a,
  .social-links .copy-email-btn {
    background-color: rgba(250, 249, 246, 0.92);
  }
}

/* Background alone — not color — applies on hover regardless of the
   copied state, so clicking the email icon doesn't make the visible
   hover background suddenly fade back down to the barely-there rest
   opacity right as the mouse is still sitting over it. */
.social-links a:hover,
.social-links a:focus-visible,
.social-links .copy-email-btn:hover {
  background-color: var(--link-hover);
  transform: translateY(-2px);
  box-shadow: 0 10px 16px -6px rgba(0, 0, 0, 0.18);
}

/* Same pressed state as the toolkit/spec badges. */
.social-links a:active,
.social-links .copy-email-btn:active {
  transform: translateY(1px);
  box-shadow: 0 1px 2px -1px rgba(0, 0, 0, 0.1);
  transition-duration: 0.05s;
}

.social-links a:hover,
.social-links a:focus-visible,
.social-links .copy-email-btn:not(.copied):hover {
  /* Dark-blue background (above) with a white icon — the stroke
     picks up the same white for free via currentColor. Excludes .copied
     so the green confirmation color (set globally on
     .copy-email-btn.copied) isn't overridden back to white while the
     mouse is still hovering right after a click. */
  color: #fff;
}

.social-links svg {
  width: 20px;
  height: 20px;
}

.social-links a .material-symbols-outlined,
.social-links .copy-email-btn .material-symbols-outlined {
  font-size: 20px;
}

/* The copy-email button swaps between two icons (mail / check)
   by toggling which one is displayed, rather than rewriting a single
   glyph's text content — works the same way whether the icon markup
   is a Material Symbols span or a hand-drawn SVG. */
.copy-email-btn .icon-copied {
  display: none;
}

.copy-email-btn.copied .icon-default {
  display: none;
}

.copy-email-btn.copied .icon-copied {
  display: inline-block;
}

/* ============================================
   12. TOAST — brief confirmation message, used after
   copy-to-clipboard actions. Created on demand by script.js.
   ============================================ */
.toast {
  position: fixed;
  left: 50%;
  bottom: 2rem;
  transform: translateX(-50%) translateY(12px);
  display: flex;
  align-items: center;
  gap: 0.45rem;
  background-color: var(--ink);
  color: var(--paper);
  padding: 0.7rem 1.25rem;
  border-radius: 8px;
  font-size: 0.85rem;
  box-shadow: 0 8px 24px -8px rgba(0, 0, 0, 0.35);
  opacity: 0;
  pointer-events: none;
  z-index: 100;
  transition: opacity 0.25s ease, transform 0.25s ease;
}

/* A brighter green than .copy-email-btn.copied's (#1d5e29) — that one's
   tuned for the light badge background, too dark to read on the toast's
   near-black one. */
.toast-icon {
  font-size: 1.1rem;
  color: #4ade80;
  flex-shrink: 0;
}

.toast.visible {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}

@media (prefers-reduced-motion: reduce) {
  .toast {
    transition: opacity 0.15s ease;
    transform: translateX(-50%);
  }
}

/* ============================================
   13. HOVER STROKE — echoes the logo's square-bracket outline.
   A masked border sweeps clockwise starting near the top-left corner
   — both drawing in on hover AND erasing back out the same clockwise
   direction it drew in, rather than un-drawing in reverse. That needs
   two independently-animatable custom properties (--stroke-start,
   --stroke-end: registered below via @property so they can actually
   transition, not just snap) instead of one: entering hover grows
   --stroke-end forward while --stroke-start stays pinned at 0%;
   leaving hover advances --stroke-start forward instead while
   --stroke-end stays pinned at 100%. A single :hover toggle can't
   drive two different properties depending on direction, so script.js
   sets these directly on mouseenter/mouseleave/focus/blur — see the
   "Hover stroke" section there.
   Applied only to standalone buttons (back-to-top, carousel arrows,
   every lightbox control) — skipped on the carousel dots (too small
   to read as a drawn line) and the zoom pill's own container (its two
   buttons already get the effect individually; a third outline around
   the whole pill on top of that reads as clutter, not polish).
   currentColor keeps the stroke matched to each button's own icon
   color, so it stays visible whether the button is light-on-dark
   (back-to-top) or dark-on-light (everything else). A conic-gradient
   mask (rather than the border-on-a-shrinking-box trick this replaced)
   also means there's no stray corner pixel at rest — the border is
   always full-size, just fully masked away until hovered.
   This is a real element (.hover-stroke-overlay, injected by
   setUpHoverStroke in script.js) rather than a ::before — a ::before
   with a var()-driven mask-image didn't reliably repaint on some
   engines when the custom properties changed, whereas a real element
   with the identical styling does. */
@property --stroke-start {
  syntax: '<percentage>';
  inherits: false;
  initial-value: 0%;
}

@property --stroke-end {
  syntax: '<percentage>';
  inherits: false;
  initial-value: 0%;
}

.hover-stroke-overlay {
  content: '';
  position: absolute;
  inset: 5px;
  /* A whole pixel, not fractional — fractional widths land on
     different sub-pixel offsets depending on each element's exact
     position, so they don't all anti-alias to the same visual
     weight. Matches .underline-hover-overlay's height for the same
     reason. */
  border: 1px solid currentColor;
  pointer-events: none;
  -webkit-mask-image: conic-gradient(from 315deg, transparent var(--stroke-start), black var(--stroke-start), black var(--stroke-end), transparent var(--stroke-end));
  mask-image: conic-gradient(from 315deg, transparent var(--stroke-start), black var(--stroke-start), black var(--stroke-end), transparent var(--stroke-end));
  transition: --stroke-start 0.4s linear, --stroke-end 0.4s linear;
}

@media (prefers-reduced-motion: reduce) {
  .hover-stroke-overlay {
    transition: none;
  }
}

/* ============================================
   14. UNDERLINE HOVER — the same sweep-in/sweep-out-the-same-
   direction idea as the button hover stroke above, but a single
   horizontal line instead of a full square: nav links, "back" links,
   case-study links, and "read more" links, so hovering any text link
   on the site feels consistent. Same two-property (--underline-start,
   --underline-end) technique for the same reason — see
   setUpSweepHover in script.js. currentColor keeps each link's
   underline matched to its own text color; .index-nav a overrides it
   to --link-hover so the nav underline stays the same dark blue the
   nav text turns on hover (currentColor would flip back to the text's
   resting color while the line is still sweeping out). */
@property --underline-start {
  syntax: '<percentage>';
  inherits: false;
  initial-value: 0%;
}

@property --underline-end {
  syntax: '<percentage>';
  inherits: false;
  initial-value: 0%;
}

.underline-hover-overlay {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  /* Whole pixel to match .hover-stroke-overlay's border-width exactly
     — see its comment for why fractional widths render inconsistently. */
  height: 1px;
  pointer-events: none;
  background: linear-gradient(to right, transparent var(--underline-start), currentColor var(--underline-start), currentColor var(--underline-end), transparent var(--underline-end));
  transition: --underline-start 0.3s linear, --underline-end 0.3s linear;
}

.index-nav a .underline-hover-overlay {
  color: var(--link-hover);
}

@media (prefers-reduced-motion: reduce) {
  .underline-hover-overlay {
    transition: none;
  }
}
/* ============================================
   CUSTOM CURSOR
   A small blue dot that tracks the pointer exactly, plus a ring that
   trails behind it. Over links and buttons the ring swells; over project
   cards and zoomable images it becomes a paper-colored label ("View",
   "Zoom"). Only switched on by script.js for a mouse/trackpad on a wide
   screen (html.has-cursor), so touch devices and small windows keep the
   native cursor. Position is driven by --cx/--cy (dot) and --rx/--ry
   (ring) set from script.js.
   ============================================ */
html.has-cursor,
html.has-cursor * {
  cursor: none !important;
}

.cursor-dot,
.cursor-ring {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 100000;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.25s ease;
}

.cursor-dot {
  width: 16px;
  height: 16px;
  /* No centering offset, unlike a circular dot — the tip of the arrow
     (its top-left corner, before the clip-path below cuts it down) is
     what should land exactly on the pointer position. */
  margin: 0;
  background: var(--link);
  /* A plain triangular arrowhead, tip at the box's own top-left corner
     (0% 0%) — simpler than a full pointer-cursor outline. */
  clip-path: polygon(0% 0%, 100% 38%, 38% 100%);
  transform: translate3d(var(--cx, -100px), var(--cy, -100px), 0);
  /* Fades back in a beat after the ring starts shrinking, so leaving a
     link reads as the halo collapsing into the dot. */
  transition: opacity 0.4s ease 0.12s;
}

.cursor-ring {
  width: 36px;
  height: 36px;
  /* Not centered on the cursor point itself — that's the dot's own tip
     (top-left corner of its box), not its visual middle. Offset by half
     the dot's 16px box (8px) so the square centers on the dot's box
     instead, and the arrow sits in the middle of the square rather than
     hugging its bottom-right corner. */
  margin: -10px 0 0 -10px;
  border-radius: 4px;
  border: 1px solid rgba(26, 26, 26, 0.2);
  transform: translate3d(var(--rx, -100px), var(--ry, -100px), 0);
  transition:
    opacity 0.25s ease,
    width 0.45s cubic-bezier(0.22, 1, 0.36, 1),
    height 0.45s cubic-bezier(0.22, 1, 0.36, 1),
    margin 0.45s cubic-bezier(0.22, 1, 0.36, 1),
    background-color 0.25s ease,
    border-color 0.25s ease,
    border-radius 0.45s cubic-bezier(0.22, 1, 0.36, 1);
  display: flex;
  align-items: center;
  justify-content: center;
}

/* Resting state inside the lightbox (not hovering the image or a
   control): the ring's usual dark ink border barely shows against the
   near-black backdrop, so it goes light instead. Placed right after the
   base rule so it's still overridden by the more specific hover states
   below (cursor-link/-button/-label-on), which set their own color. */
html.lightbox-open .cursor-ring {
  border-color: rgba(255, 255, 255, 0.35);
}

.cursor-label {
  font-size: 0.8rem;
  letter-spacing: 0.02em;
  color: var(--ink);
  opacity: 0;
  transition: opacity 0.2s ease;
  white-space: nowrap;
}

html.cursor-visible .cursor-dot,
html.cursor-visible .cursor-ring {
  opacity: 1;
}

/* Over a text link: the ring swells into a soft blue halo. */
html.cursor-link .cursor-ring {
  width: 120px;
  height: 120px;
  margin: -60px 0 0 -60px;
  border-color: rgba(36, 85, 230, 0.35);
  background-color: rgba(36, 85, 230, 0.08);
}

/* Over a round, icon-only control (social icons, carousel prev/next and
   dots, back to top) — same halo, 30px smaller than a text link's, so
   it doesn't swallow up neighbouring icons the way the full-size one
   would over a tightly packed row. */
html.cursor-button .cursor-ring {
  width: 90px;
  height: 90px;
  margin: -45px 0 0 -45px;
  border-color: rgba(36, 85, 230, 0.35);
  background-color: rgba(36, 85, 230, 0.08);
}

/* The lightbox's own buttons (close, prev/next, zoom in/out) are the
   only thing that can trigger cursor-button while the lightbox is open
   — lighter, matching the magnifying-lens lens's own lighter blue,
   since it's sitting over the same near-black backdrop. */
html.lightbox-open.cursor-button .cursor-ring {
  border-color: rgb(110, 145, 252);
  background-color: rgba(110, 145, 252, 0.12);
}

html.cursor-link .cursor-dot,
html.cursor-button .cursor-dot {
  opacity: 0.2;
  width: 2px;
  height: 2px;
  margin: -1px 0 0 -1px;
  transition-delay: 0s;
  transition-duration: 0.2s;
}

/* Over a project card, case-study card or the lightbox image: a labelled
   translucent blue disc. White text — the disc's tint is too light to
   read dark text on over a photo or (in the lightbox) the near-black
   backdrop. */
html.cursor-label-on .cursor-ring {
  width: 60px;
  height: 60px;
  margin: -30px 0 0 -30px;
  background-color: rgba(36, 85, 230, 0.2);
  border-color: rgb(110, 145, 252);
}
/*html.cursor-label-on .cursor-label {
  opacity: 1;
  color: #fff;
}*/
html.cursor-label-on .cursor-dot {
  background-color: rgb(202, 216, 255);
  opacity: 0.2;
  width: 2px;
  height: 2px;
  margin: -1px 0 0 -1px;
}

/* Over a zoomable image: a magnifying lens — a ring whose inside (.cursor-lens) shows the image
   under it magnified; script.js paints its background. Square, like the
   ring's own resting shape (border-radius: 4px, inherited from .cursor-ring). */
html.cursor-plus .cursor-ring {
  width: 180px;
  height: 180px;
  margin: -90px 0 0 -90px;
  border: 1.5px solid var(--link);
  background-color: rgba(36, 85, 230, 0.06);
}
html.cursor-plus .cursor-dot {
  opacity: 0;
  transition-delay: 0s;
  transition-duration: 0.2s;
}
.cursor-lens {
  position: absolute;
  inset: 0;
  border-radius: 4px;
  background-repeat: no-repeat;
  opacity: 0;
  transition: opacity 0.25s ease;
}
html.cursor-plus .cursor-lens {
  opacity: 1;
}

/* Inside the open lightbox, hovering the image: an empty magnifying-lens
   shape — round, with a short handle at its lower right — and nothing
   inside it (no icon, no text, no magnified preview like .cursor-plus
   above has, since the lightbox image is already the full-size view).
   A lighter blue than the rest of the cursor's blue states. */
html.cursor-lightbox-lens .cursor-ring {
  width: 64px;
  height: 64px;
  margin: -32px 0 0 -32px;
  border-radius: 50%;
  border: 1.5px solid rgb(110, 145, 252);
  background-color: rgba(110, 145, 252, 0.12);
}
html.cursor-lightbox-lens .cursor-dot {
  opacity: 0;
  transition-delay: 0s;
  transition-duration: 0.2s;
}
html.cursor-lightbox-lens .cursor-label,
html.cursor-lightbox-lens .cursor-lens {
  opacity: 0 !important;
}
/* The handle, touching the ring's edge and pointing away at 45°. A
   translate(tx, ty) with tx = ty moves the handle's center a diagonal
   distance of tx*sqrt(2), not tx itself — so to land that center exactly
   (ring radius + half the handle's own length) out from the ring's
   center, tx has to be that distance divided by sqrt(2), not the
   distance itself (an earlier version of this used the distance
   directly, which left a gap between the handle and the ring). */
html.cursor-lightbox-lens .cursor-ring::before {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: 20px;
  height: 4px;
  margin: -2px 0 0 -10px;
  background: rgb(110, 145, 252);
  border-radius: 2px;
  transform: translate(29.7px, 29.7px) rotate(45deg);
}

/* Pressed */
html.cursor-down .cursor-ring {
  transform: translate3d(var(--rx, -100px), var(--ry, -100px), 0) scale(0.88);
}

@media (prefers-reduced-motion: reduce) {
  .cursor-ring {
    transition: opacity 0.25s ease;
  }
}

/* On phones the back link above a project's title is left out — the
   header's own navigation and the link at the bottom of the page do the
   job, and it just pushes the title down. */
@media (max-width: 680px) {
  .project-header .project-nav {
    display: none;
  }
}

/* No sideways scrolling on phones and tablets. Some things sit wider than
   the screen on purpose or by accident — carousel slides waiting off to
   the side, the language tooltips before they're hovered — and would
   otherwise make the whole page pannable. clip (unlike hidden) doesn't
   turn the page into a scroll container, so the sticky header and
   scroll-based effects keep working. */
@media (max-width: 1024px) {
  html,
  body {
    overflow-x: clip;
  }
}

/* The spec blocks (Role, Tools, ...) have widths sized for their badges;
   on the narrowest phones cap them at the column instead of overflowing. */
@media (max-width: 480px) {
  .badge-block {
    max-width: 100%;
  }
}

/* On phones the "Next project" / "Next case study" links drop the name of
   what comes next ("Next project: Omnitrack Website →" becomes "Next
   project →") so they fit on one line. */
@media (max-width: 680px) {
  .nav-detail {
    display: none;
  }
}

/* ============================================
   404 PAGE
   ============================================ */
.not-found-section {
  text-align: center;
  padding-top: 8rem;
  padding-bottom: 8rem;
}

.not-found-code {
  font-family: var(--display-font);
  font-size: clamp(4rem, 14vw, 7rem);
  font-weight: 700;
  line-height: 1;
  color: var(--accent);
  margin: 0 0 1rem;
}

.not-found-section h1 {
  font-family: var(--display-font);
  font-size: clamp(1.5rem, 4vw, 2rem);
  line-height: 1.15;
  margin: 0 0 1rem;
}

.not-found-section p {
  font-size: clamp(1rem, 3vw, 1.1rem);
  line-height: 1.8rem;
  color: var(--muted);
  max-width: 420px;
  margin-left: auto;
  margin-right: auto;
}

.not-found-section p + p {
  margin-top: 1.5rem;
}
