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

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

ul {
  list-style: none;
}

a {
  color: inherit;
  text-decoration: none;
}

body {
  font-family: var(--font-family);
  font-size: var(--font-size-base);
  line-height: var(--line-height-base);
  color: var(--color-text);
  background: var(--color-bg);
  padding-inline: var(--space-page);
}

:root {
  --color-bg: #FEFEFE;
  --color-bg-muted: #f8f9fa;
  --color-text: #202528;
  --color-muted: #d9d9d9;

  --font-family: "Ladna Trial", sans-serif;

  --font-size-xl: 4rem;      /* 64px */
  --font-size-lg: 2rem;      /* 32px */
  --font-size-base: 1rem;    /* 16px */
  --font-size-sm: 0.8125rem; /* 13px */

  --line-height-xl: 0.9375;  /* 60px / 64px */
  --line-height-lg: 1.25;    /* 40px / 32px */
  --line-height-base: 1.5;   /* 24px / 16px */
  --line-height-sm: 1.846;   /* 24px / 13px */

--space-3xs: 0.25rem;  /* 4px */
--space-2xs: 0.625rem; /* 10px */
--space-sm: 1rem;      /* 16px */
--space-md: 1.5rem;    /* 24px */
--space-lg: 2rem;      /* 32px */
--space-xl: 3rem;      /* 48px */
--space-2xl: 4rem;     /* 64px */
--space-3xl: 8rem;     /* 128px */
--space-4xl: 16rem;    /* 256px */

--space-section: var(--space-3xl);
--space-page: var(--space-4xl);
--space-block-gap: 6rem; /* 96px — default gap between case-study content blocks */

--content-max-width: 76rem; /* 1216px */

--radius-sm: 2px;
}

@font-face {
  font-family: "Ladna Trial";
  src: url("../assets/fonts/LadnaTrial-Regular.otf") format("opentype");
  font-weight: 400;
  font-style: normal;
}

@font-face {
  font-family: "Ladna Trial";
  src: url("../assets/fonts/LadnaTrial-Bold.otf") format("opentype");
  font-weight: 700;
  font-style: normal;
}

@keyframes fade-in-up {
  from {
    opacity: 0;
    transform: translateY(1rem);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.reveal {
  opacity: 0;
  transform: translateY(1rem);
  transition: opacity 0.6s ease-out, transform 0.6s ease-out;
}

.reveal.is-visible {
  opacity: 1;
  transform: translateY(0);
}

.site-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: var(--space-3xl);
  margin-inline: calc(-1 * var(--space-page));
  padding-inline: var(--space-2xl);
  position: sticky;
  top: 0;
  z-index: 10;
}

/* The glass is painted by a pseudo-element rather than by .site-header itself,
   and that is load-bearing, not stylistic.

   An element with backdrop-filter becomes a BACKDROP ROOT: its descendants'
   own backdrop-filters then sample what is painted inside that root, not the
   page behind it. The mobile dropdown is a descendant of .site-header, so
   while the header carried the filter directly, the dropdown declared the
   exact same blur and got nothing to blur - it sampled the header's own empty
   backdrop and rendered as flat 70% white with the page showing through
   perfectly sharp underneath.

   Moving the filter to ::before makes the PSEUDO the backdrop root. The
   dropdown is its sibling, not its descendant, so it escapes the root and
   samples the real page - the same glass, independently.

   inset: 0 resolves against the header's padding box, which is the full width
   including the padding, so this still covers edge to edge. */
.site-header::before {
  content: "";
  position: absolute;
  inset: 0;
  background: color-mix(in srgb, var(--color-bg) 70%, transparent);
  backdrop-filter: blur(20px) saturate(180%);
  -webkit-backdrop-filter: blur(20px) saturate(180%);
}

/* An absolutely positioned ::before paints above static in-flow siblings, so
   without this the glass would cover the logo and the nav. Lifting the
   children is deliberately done with z-index ALONE and no position: relative -
   they are flex items, and z-index applies to flex items even when static.
   Adding position: relative to <nav> would make it the containing block for
   the mobile dropdown, whose left: 0/right: 0 would then span only the nav
   instead of the full header.

   Doing it this way also avoids giving ::before a negative z-index, which
   Safari has historically mishandled in combination with backdrop-filter. */
.site-header > * {
  z-index: 1;
}

.site-header nav ul{
  display: flex;
  gap: var(--space-lg);
}

#hero {
  display: flex;
  align-items: center;
  gap: var(--space-section);
  padding-block: var(--space-section);
  max-width: var(--content-max-width);
  margin-inline: auto;

  .hero-text{
    flex: 1;
  }

  .hero-text p,
  .hero-text h1 {
    font-size: var(--font-size-lg);
    line-height: var(--line-height-lg);
    font-weight: 400;
  }

  .hero-text p {
    margin-bottom: var(--space-2xl);
  }

  .hero-text h1 strong {
    font-weight: 700;
  }

  .hero-text p,
  .hero-text h1,
  .hero-portrait {
    animation: fade-in-up 0.6s ease-out both;
  }

  .hero-text h1 {
    animation-delay: 0.15s;
  }

  .hero-portrait {
    animation-delay: 0.3s;
  }

  .hero-portrait {
    width: 230px;
    height: 230px;
    flex-shrink: 0;
    border-radius: var(--radius-sm);
    object-fit: cover;
  }
}

#works {
  display: flex;
  flex-direction: column;
  gap: var(--space-section);
  padding-block: var(--space-section);
  max-width: var(--content-max-width);
  margin-inline: auto;
}

.works-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  grid-template-rows: auto auto;
  row-gap: var(--space-md);
  column-gap: 5.25rem;
}

.work {
  display: grid;
  grid-template-rows: subgrid;
  grid-row: span 2;
  row-gap: var(--space-md);
}

.work-header h2 {
  font-size: var(--font-size-lg);
  line-height: var(--line-height-lg);
  font-weight: 400;
}

.work-header {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}

.work-meta {
  display: flex;
  gap: var(--space-md);
}

.work-meta p {
  flex: 1;
}

.work-image {
  width: 100%;
  border-radius: var(--radius-sm);
}

#contact {
  padding-block: var(--space-4xl);
  max-width: var(--content-max-width);
  margin-inline: auto;
  text-align: center;
}

#contact h2 {
  font-size: var(--font-size-xl);
  line-height: var(--line-height-xl);
  font-weight: 400;
  margin-bottom: var(--space-sm);
}

#contact p {
  margin-bottom: var(--space-lg);
}

.contact-email,
.resume-download {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 15.625rem;
  min-height: 2.75rem;
  padding: var(--space-3xs) var(--space-sm);
  background: var(--color-text);
  color: white;
  border-radius: var(--radius-sm);
  font-size: var(--font-size-base);
  font-weight: 500;
  line-height: var(--line-height-base);
}

.experience-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  column-gap: var(--space-3xl);
  row-gap: var(--space-xl);
  align-self: stretch;
}

.experience-item {
  display: flex;
  flex-direction: column;
  gap: var(--space-3xs);
}

.experience-item h3 {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-weight: 700;
}

.experience-dots {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  flex-shrink: 0;
  border: 1px solid var(--color-muted);
  border-radius: 50%;
  transition: border-color 0.2s ease;
}

.experience-dots::before {
  content: "";
  width: 2px;
  height: 2px;
  border-radius: 50%;
  background: var(--color-muted);
  box-shadow: 4.69px 0 0 var(--color-muted), -4.69px 0 0 var(--color-muted);
  transition: background 0.2s ease, box-shadow 0.2s ease;
}

.experience-item.is-open .experience-dots {
  border-color: var(--color-text);
}

.experience-item.is-open .experience-dots::before {
  background: var(--color-text);
  box-shadow: 4.69px 0 0 var(--color-text), -4.69px 0 0 var(--color-text);
}

.experience-item::after {
  content: "";
  margin-top: var(--space-sm);
  border-top: 1px solid var(--color-muted);
  transition: border-color 0.2s ease;
}

.experience-item.is-open::after {
  border-top-color: var(--color-text);
}

.experience-item {
  position: relative;
  z-index: 0;
}

.experience-item.is-open {
  z-index: 2;
}

.experience-panel {
  display: flex;
  position: absolute;
  top: 100%;
  left: 0;
  margin-top: var(--space-sm);
  width: 28.75rem;
  flex-direction: column;
  gap: var(--space-sm);
  padding: var(--space-sm);
  background: var(--color-bg);
  border: 1px solid var(--color-muted);
  border-radius: var(--radius-sm);
  box-shadow: 0 2px 8px rgba(32, 37, 40, 0.1);
  font-size: var(--font-size-sm);
  line-height: 1.25rem;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transform: translateY(-0.5rem);
  transition: opacity 0.2s ease, transform 0.2s ease, visibility 0.2s;
}

.experience-item.is-open .experience-panel {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  transform: translateY(0);
}

.experience-cards {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-sm);
}

.experience-card h4 {
  font-weight: 700;
}

#resume {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding-block: var(--space-section);
  max-width: var(--content-max-width);
  margin-inline: auto;
}

.resume-download {
  margin-top: var(--space-xl);
}

#footer {
  display: flex;
  justify-content: space-between;
  padding-top: var(--space-2xl);
  padding-bottom: var(--space-2xl);
  max-width: var(--content-max-width);
  margin-inline: auto;
  color: var(--color-muted);
}

/* Together with #footer's own padding-top this makes 128px between the last
   section and the footer text, per the Figma frame. The homepage doesn't need
   the equivalent - its closing sections carry that much padding already. */
#case-study-sections {
  padding-bottom: var(--space-2xl);
}

#case-study-hero {
  margin-inline: calc(-1 * var(--space-page));
}

/* The hero is a video on Amenti and a still on Bigbank, so both element types
   share one rule. --hero-aspect is set per project by renderCaseStudyHero;
   3 / 1 stays the fallback for anything that does not declare one, and as of
   2026-08-20 every project declares that same 3 / 1 anyway - the heroes are
   meant to be one consistent height. */
#case-study-hero video,
#case-study-hero img {
  display: block;
  width: 100%;
  aspect-ratio: var(--hero-aspect, 3 / 1);
  object-fit: cover;
}

#case-study-title {
  padding-right: 12rem;
  padding-top: var(--space-lg);
  margin-top: var(--space-2xl);
}

#case-study-title h1 {
  font-size: var(--font-size-xl);
  line-height: var(--line-height-xl);
  font-weight: 400;
}

.case-study-meta {
  margin-top: var(--space-xl);
  padding-block: var(--space-lg);
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}

.meta-row {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-sm);
}

.meta-divider {
  border-top: 1px solid var(--color-muted);
}

/* Centred on the viewport as a whole block, whatever height the project's
   own list of sections gives it - clicking a dot scrolls that section's
   heading up to meet the list's top edge, so the nav itself never moves. */
.progress-nav {
  position: fixed;
  top: 50%;
  right: var(--space-3xl);
  transform: translateY(-50%);
  z-index: 10;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.12s ease;
}

.progress-nav.is-visible {
  opacity: 1;
  pointer-events: auto;
  transition: opacity 0.3s ease;
}

.progress-nav ul {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}

.progress-item button {
  display: flex;
  width: 100%;
  align-items: center;
  justify-content: flex-end;
  gap: 12px;
  background: none;
  border: none;
  padding: 0;
  appearance: none;
  -webkit-appearance: none;
  cursor: pointer;
}

.progress-label-wrap {
  display: grid;
  grid-template-columns: 0fr;
  transition: grid-template-columns 0.3s ease;
}

.progress-label {
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-align: right;
  font-size: var(--font-size-sm);
  color: var(--color-muted);
  opacity: 0;
  transition: opacity 0.2s ease, color 0.2s ease;
}

.progress-item:hover .progress-label-wrap,
.progress-item.is-active .progress-label-wrap {
  grid-template-columns: 1fr;
}

.progress-item:hover .progress-label,
.progress-item.is-active .progress-label {
  opacity: 1;
}

.progress-item.is-active .progress-label {
  color: var(--color-text);
}

.progress-dot {
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: var(--color-bg-muted);
  border: 0.5px solid var(--color-muted);
  box-shadow: 0 2px 8px rgba(32, 37, 40, 0.1);
  flex-shrink: 0;
  transition: background-color 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
}

.progress-item.is-active .progress-dot {
  background: var(--color-text);
  border-color: var(--color-text);
  box-shadow: none;
}

@media (max-width: 1024px) {
  .progress-nav {
    display: none;
  }
}

.case-study-section {
  --section-padding-right: 12rem;
  display: flex;
  flex-direction: column;
  gap: var(--space-lg);
  padding-right: var(--section-padding-right);
  margin-top: var(--space-block-gap);
}

.case-study-section > .section-divider {
  margin-bottom: calc(var(--space-block-gap) - var(--space-lg));
}

.case-study-section > .media {
  margin-top: calc(var(--space-block-gap) - var(--space-lg));
}

.case-study-section > .subsections {
  margin-top: calc(var(--space-block-gap) - var(--space-lg));
}

.case-study-section > h2 + .subsections {
  margin-top: 0;
}

.case-study-section h2 {
  font-size: var(--font-size-lg);
  line-height: var(--line-height-lg);
  font-weight: 400;
}

.media {
  display: block;
  max-width: 75%;
  border-radius: var(--radius-sm);
}

.media--full {
  width: calc(100% + (2 * var(--space-page)) + var(--section-padding-right));
  max-width: none;
  margin-left: calc(-1 * var(--space-page));
  margin-right: calc(-1 * var(--space-page) - var(--section-padding-right));
  /* auto is the initial value, so a band with no declared ratio behaves
     exactly as before and stays governed by max-height. renderMedia sets the
     property from the data where a frame crops the band to a fixed shape. The
     tablet block's own aspect-ratio on .media--full still wins by source
     order, which is the point of using a property instead of an inline style. */
  aspect-ratio: var(--media-aspect, auto);
  max-height: 640px;
  object-fit: cover;
  border-radius: 0;
}

/* An inline link inside body copy. Figma draws these bold + underlined, which
   is the whole affordance - there is no colour change to lean on. */
.inline-link {
  font-weight: 700;
  text-decoration: underline;
  text-underline-offset: 0.2em;
  transition: opacity 0.2s ease;
}

.inline-link:hover {
  opacity: 0.6;
}

.bullet-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
  padding-left: var(--space-md);
}

.bullet-list li {
  list-style: disc;
}

.stats-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-2xl);
}

.case-study-section > .stats-row {
  margin-top: calc(var(--space-md) - var(--space-lg));
}

.stat-card,
.chip-list li {
  background: var(--color-bg-muted);
  border-radius: var(--radius-sm);
  box-shadow: 0 2px 8px rgba(32, 37, 40, 0.1);
  padding: var(--space-sm);
}

.stat-card {
  padding-top: var(--space-md);
}

.stat-card p {
  margin-bottom: var(--space-3xs);
}

.stat-card span {
  display: block;
  font-size: var(--font-size-lg);
}

.chip-list {
  display: flex;
  flex-wrap: wrap;
  column-gap: var(--space-lg);
  row-gap: var(--space-sm);
}

.chip-list li {
  height: 48px;
  display: flex;
  align-items: center;
  padding: 0 var(--space-sm);
}

.chip-list li.chip-list__item--inverted {
  background: var(--color-text);
  color: var(--color-bg);
}

.section-divider {
  height: 1px;
  background: var(--color-muted);
  width: calc(100% + (2 * var(--space-page)) + var(--section-padding-right));
  margin-left: calc(-1 * var(--space-page));
  margin-right: calc(-1 * var(--space-page) - var(--section-padding-right));
}

.subsections {
  display: flex;
  flex-direction: column;
  gap: var(--space-block-gap);
}

.subsections--tight {
  gap: var(--space-lg);
}

.subsection {
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
}

.subsection h3 {
  font-size: var(--font-size-base);
  font-weight: 600;
}

.subsection-label {
  font-size: var(--font-size-sm);
  color: var(--color-muted);
}

.paragraph-group {
  display: flex;
  flex-direction: column;
  gap: var(--space-lg);
}

.gallery-figure {
  display: inline-flex;
  flex-direction: column;
  gap: var(--space-md);
}

.gallery {
  display: flex;
  flex-direction: row;
  gap: 0.375rem;
}

.gallery-image {
  display: block;
  height: 15.5rem;
  width: auto;
  border-radius: var(--radius-sm);
}

.gallery-caption {
  max-width: 75%;
  font-size: var(--font-size-sm);
  text-align: center;
}

.gallery-figure .gallery-caption {
  max-width: none;
}

/* A caption belonging to a full-bleed media has to centre on the bleed, not on
   the content column, so it repeats .media--full's own width/margin calc. Those
   are all token-based, so this follows the token overrides at each breakpoint
   without needing a copy there. The negative margin-top trims the section's
   32px flex gap down to the 16px the frame puts between image and caption. */
.case-study-section > .media--full + .gallery-caption {
  width: calc(100% + (2 * var(--space-page)) + var(--section-padding-right));
  max-width: none;
  margin-left: calc(-1 * var(--space-page));
  margin-right: calc(-1 * var(--space-page) - var(--section-padding-right));
  margin-top: calc(var(--space-sm) - var(--space-lg));
}

/* Two-column screenshot grid. .gallery is a single row sharing one height; this
   one keeps every image at its own aspect. --span-first puts a tall screen in
   the left column across both rows, beside a stacked pair on the right - the
   row track is sized by that pair, so the tall one covers the few px of
   difference rather than stretching. */
.image-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-sm);
}

.image-grid img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: var(--radius-sm);
}

.image-grid--span-first > img:first-child {
  grid-row: span 2;
  height: 100%;
  /* The row track is sized by the stacked pair on the right, which comes out a
     few px taller than this image is at this width. `cover` would scale up to
     close that gap and shave ~2px off each side - enough to eat the
     screenshot's border. `contain` keeps it whole and splits the difference
     above and below, where it's invisible against the page. */
  object-fit: contain;
}

.carousel {
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
}

.carousel-card {
  display: flex;
  align-items: flex-start;
  gap: var(--space-lg);
  background: var(--color-bg-muted);
  border-radius: var(--radius-sm);
  box-shadow: 0 2px 8px rgba(32, 37, 40, 0.1);
  padding: var(--space-md);
  /* The card's height is animated between quotes of differing length; without
     this a taller incoming quote spills outside the card while the box is
     still catching up. Doesn't clip the shadow, which paints outside. */
  overflow: hidden;
}

.carousel-person {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  flex-shrink: 0;
}

.carousel-avatar {
  width: 70px;
  height: 70px;
  border-radius: var(--radius-sm);
  object-fit: cover;
  object-position: top;
}

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

.carousel-quote {
  align-self: center;
}

.carousel-controls {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2xl);
}

.carousel-arrow {
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: none;
  padding: 0;
  color: var(--color-muted);
  cursor: pointer;
  transition: color 0.2s ease;
}

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

.carousel-dot {
  background: none;
  border: none;
  padding: var(--space-2xs);
  margin: calc(-1 * var(--space-2xs));
  font-size: var(--font-size-sm);
  color: var(--color-muted);
  cursor: pointer;
}

.carousel-dot.is-active {
  color: var(--color-text);
  font-weight: 700;
}

.feature-slide {
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
  /* Same reason as .carousel-card: the height is animated between slides
     whose captions wrap to different numbers of lines. */
  overflow: hidden;
}

.feature-image {
  display: block;
  width: 100%;
  height: auto;
  /* Every feature screenshot is exported from Figma at 2400x1324, so the box
     can hold its final height before the file itself arrives. Without this a
     not-yet-loaded image measures zero, the slide collapses to just its
     caption, and the swap animation takes that collapsed height as its
     target - which reads as the image flashing away and back. */
  aspect-ratio: 2400 / 1324;
  border-radius: var(--radius-sm);
}

.feature-caption {
  font-size: var(--font-size-sm);
  text-align: center;
}

.insight-cards {
  display: flex;
  gap: var(--space-sm);
}

.insight-card {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  /* 11px, not the 16 the box wants, and the 5px difference is optical.
     Measured ink-to-ink on the rendered card: 16px padding + the icon's own
     12.5px of internal whitespace put 29px above the drawing, but below it the
     same 12.5 + 16 is followed by the title's half-leading - the gap between
     its line box and the cap of the letters - which added 5 more, so the space
     read as 34 against 29. Geometrically balanced, visually not.

     Trimming the gap by exactly that 5 makes both sides measure 29 to the ink,
     which is what the eye actually compares. (text-box-trim would express this
     properly but is too new to rely on.) */
  gap: 0.6875rem;
  padding: var(--space-sm) var(--space-sm) var(--space-md);
  border-radius: var(--radius-sm);
  background: linear-gradient(180deg, rgba(217, 217, 217, 0.15) 0%, #ffc1c1 100%);
}

.insight-card img {
  width: 64px;
  height: 64px;
  border-radius: var(--radius-sm);
  object-fit: cover;
}

.jtbd-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
  background: var(--color-bg-muted);
  border-radius: var(--radius-sm);
  box-shadow: 0 2px 8px rgba(32, 37, 40, 0.1);
  padding: var(--space-md);
}

.jtbd-items {
  display: flex;
  gap: var(--space-sm);
}

.jtbd-item {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: var(--space-3xs);
}

.jtbd-item strong {
  font-size: var(--font-size-base);
  font-weight: 700;
}

.jtbd-item span {
  font-size: var(--font-size-sm);
}

.journey-cards {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
}

.journey-card {
  flex: 1 1 150px;
  display: flex;
  flex-direction: column;
  gap: var(--space-3xs);
  padding: var(--space-sm);
  border-radius: var(--radius-sm);
  background: var(--color-bg-muted);
  box-shadow: 0 2px 8px rgba(32, 37, 40, 0.1);
}

.journey-card strong {
  font-size: var(--font-size-sm);
  font-weight: 700;
}

.journey-card span {
  font-size: var(--font-size-sm);
}

.nav-toggle {
  display: none;
  flex-direction: column;
  justify-content: center;
  gap: 5px;
  width: 24px;
  height: 24px;
  padding: 0;
  margin: 0;
  background: none;
  border: none;
  appearance: none;
  -webkit-appearance: none;
  line-height: 0;
  cursor: pointer;
}

.nav-toggle span {
  display: block;
  width: 100%;
  height: 2px;
  background: var(--color-text);
}

@media (max-width: 1024px) {
  :root {
    --space-page: var(--space-2xl);
    --space-section: var(--space-xl);
    --font-size-xl: 3rem;
    --font-size-lg: 1.75rem;
  }

  #hero {
    flex-direction: column;
    align-items: flex-start;
  }

  .works-row {
    grid-template-columns: 1fr;
  }

  .experience-grid {
    grid-template-columns: repeat(2, 1fr);
    column-gap: var(--space-lg);
  }

  .experience-panel {
    width: 100%;
  }
}

@media (max-width: 640px) {
  :root {
    --space-page: var(--space-md);
    --space-section: var(--space-lg);
    --font-size-xl: 2.25rem;
    --font-size-lg: 1.5rem;
  }

  .site-header {
    height: var(--space-2xl);
    margin-inline: calc(-1 * var(--space-page));
    padding-inline: var(--space-sm);
  }

  .nav-toggle {
    display: flex;
    margin-left: auto;
  }

  .site-header nav ul {
    display: none;
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    flex-direction: column;
    align-items: flex-end;
    gap: var(--space-md);
    padding: var(--space-md);
    background: color-mix(in srgb, var(--color-bg) 70%, transparent);
    backdrop-filter: blur(20px) saturate(180%);
    -webkit-backdrop-filter: blur(20px) saturate(180%);
    border-bottom: 1px solid var(--color-muted);
  }

  .site-header nav ul.is-open {
    display: flex;
  }

  .hero-portrait {
    width: 160px;
    height: 160px;
  }

  .experience-grid {
    grid-template-columns: 1fr;
  }

  .work-meta {
    flex-direction: column;
    gap: var(--space-3xs);
  }

  .contact-email,
  .resume-download {
    min-width: 0;
    width: 100%;
  }

  #footer {
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-sm);
  }
}
/* ══════════════════════════════════════════════════════════════════════════
   MOBILE-FIGMA · 2026-08-18 · START
   Homepage mobile pass, matched to Figma frames 80:181 (default) and
   80:735 (experience open) in Portfolio_2026.

   Everything for this pass lives between this marker and the END marker, so
   the whole thing can be deleted in one piece. It is appended last on purpose:
   it overrides the earlier @media (max-width: 640px) block by source order
   rather than by editing it, which keeps the original rules intact and
   readable underneath.

   Paired with the MOBILE-FIGMA markers in js/render.js and index.html.
   Case-study pages are deliberately untouched.
   ══════════════════════════════════════════════════════════════════════════ */

/* --- applies at every width: the accordion markup now exists everywhere --- */

/* Transparent to layout on desktop, so .experience-panel still positions
   against .experience-item and hover behaves exactly as it did before. */
/* Kept in step with .site-header's height so an anchored section lands just
   under the sticky header instead of behind it. */
:root {
  --header-height: var(--space-3xl);   /* 128px, matches .site-header */
}

#contact,
#resume {
  scroll-margin-top: var(--header-height);
}

/* Works lands on the first card rather than on the section's empty top
   padding, so the cover image is fully in view - on desktop the cover sits
   below the title, so the padding was pushing it off the bottom. Subtracting
   the section's own padding puts the card flush under the header. Both terms
   are breakpoint-aware, so this holds at every width. */
#works {
  scroll-margin-top: calc(var(--header-height) - var(--space-section));
}

.experience-reveal,
.experience-clip {
  display: contents;
}

.experience-trigger {
  display: flex;
  flex-direction: column;
  gap: var(--space-3xs);
  width: 100%;
  padding: 0;
  background: none;
  border: 0;
  appearance: none;
  -webkit-appearance: none;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
}

/* The <h3> is now only a semantic wrapper around the button; the flex row that
   pairs the company name with its dots moved down to .experience-company. */
.experience-item h3 {
  display: block;
  font-weight: 400;
}

.experience-company {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-weight: 700;
}

@media (max-width: 640px) {
  :root {
    --space-page: 1.5rem;               /* 24px gutters: 402 frame - 354 content */
    --space-section: var(--space-2xl);  /* 64px */
  }

  /* Type sizes are set on the homepage's own selectors rather than on the
     shared --font-size-* tokens, so the case-study pages keep their current
     mobile type until that pass happens. */
  #contact h2 {
    font-size: 2rem;        /* 32px */
    line-height: 1.25;      /* 40px; the 0.9375 token ratio is desktop display type */
  }

  .work-header h2 {
    font-size: 2rem;        /* 32px */
    line-height: 1.25;      /* 40px */
  }

  :root {
    --header-height: 4.5rem;            /* 72px */
  }

  /* Header: full-bleed blur, content aligned to the gutter, 72px tall */
  .site-header {
    height: var(--header-height);
    padding-inline: var(--space-page);
  }

  /* Two bars of 29x4, 8px apart. The box is padded out to 44x44 for the tap
     target, then pulled back by an equal negative margin so it still occupies
     29x20 on screen. Same trick as .carousel-dot. */
  .nav-toggle {
    width: 44px;
    height: 44px;
    gap: 8px;
    align-items: center;
    justify-content: center;
    margin: -12px -7.5px -12px auto;
  }

  .nav-toggle span {
    width: 29px;
    height: 4px;
  }

  /* Case-study pages still ship three bars; hide the third there too. */
  .nav-toggle span:nth-child(3) {
    display: none;
  }

  /* Section rhythm. Figma's root auto-layout puts 32px between every section
     on top of each section's own 64px padding. */
  #hero,
  #works,
  #contact,
  #resume {
    padding-block: var(--space-2xl);
  }

  main > section + section {
    margin-top: var(--space-lg);
  }

  /* Hero: portrait above the text, and no bottom padding - the Figma frame
     carries none, the 32px root gap does that job. */
  #hero {
    gap: var(--space-lg);
    padding-bottom: 0;
  }

  #hero .hero-portrait {
    width: 175px;
    height: 175px;
    order: -1;
  }

  #hero .hero-text p,
  #hero .hero-text h1 {
    font-size: 1.5rem;      /* 24px */
    line-height: 1.1667;    /* 28px */
  }

  #hero .hero-text p {
    margin-bottom: 1.75rem; /* 28px: one empty line in the Figma text block */
  }

  /* Works: cover image first, then title and meta */
  .works-row {
    row-gap: var(--space-2xl);
  }

  .work {
    /* subgrid only aligns header/image across columns, and there is one column
       here. Flex column-reverse puts the cover above the title without relying
       on how `order` resolves inside a subgrid. */
    display: flex;
    flex-direction: column-reverse;
    gap: var(--space-sm);
  }

  .work-image {
    /* Bigbank and Flo Kasearu are 1.68 natively but 370x191 (1.937) in Figma,
       so Figma is cropping them. Match that instead of letting them run tall. */
    aspect-ratio: 370 / 191;
    object-fit: cover;
  }

  .work-meta {
    gap: 0.5rem;
  }

  /* Contact: 250px button, centred, not full width */
  #contact p {
    margin-bottom: var(--space-md);
  }

  .contact-email,
  .resume-download {
    width: auto;
    min-width: 15.625rem;
  }

  /* CV: centred rows, 320px rule inset 25px each side */
  .experience-item {
    align-items: center;
    text-align: center;
    gap: 0;
  }

  .experience-item h3 {
    align-self: stretch;
  }

  .experience-trigger {
    gap: 2px;
    align-items: center;
    text-align: center;
  }

  .experience-company {
    justify-content: center;
  }

  .experience-item::after {
    align-self: stretch;
    margin-top: 6px;
  }

  /* The accordion. Height animates via grid-template-rows 0fr -> 1fr, the same
     technique .progress-label-wrap already uses on the column axis. The margin
     animates alongside so a closed entry leaves no gap behind. */
  .experience-reveal {
    display: grid;
    grid-template-rows: 0fr;
    align-self: stretch;
    margin-top: 0;
    /* ::after (the rule) is a generated last child at order 0, so the panel has
       to be pushed past it to open *below* the line, as in frame 80:735. */
    order: 1;
    transition: grid-template-rows 0.3s ease, margin-top 0.3s ease;
  }

  /* Bare wrapper - no padding or border of its own, so the row really can reach
     zero. The 12px padding is purely room for the panel's drop shadow, which
     overflow:hidden would otherwise cut off; the equal negative margin means it
     costs no layout space, closed or open. */
  .experience-clip {
    display: block;
    min-height: 0;
    overflow: hidden;
    margin: -12px;
    padding: 12px;
  }

  .experience-item.is-open .experience-reveal {
    grid-template-rows: 1fr;
    margin-top: var(--space-sm);
  }

  .experience-panel {
    position: static;
    width: auto;
    margin-top: 0;
    transform: none;
    text-align: left;
  }

  .experience-cards {
    grid-template-columns: 1fr;
  }

  /* Footer: centred, 10px between the two lines */
  #footer {
    align-items: center;
    text-align: center;
    font-size: var(--font-size-sm);   /* 13px */
    gap: var(--space-3xs);            /* 4px */
    padding-top: var(--space-lg);
    padding-bottom: var(--space-lg);
  }
}
/* ══════════════════════════════════════════════════════════════════════════
   MOBILE-FIGMA · 2026-08-18 · END
   ══════════════════════════════════════════════════════════════════════════ */


/* ══════════════════════════════════════════════════════════════════════════
   MOBILE-FIGMA-CS · 2026-08-18 · START
   Case-study mobile pass, matched to Figma frame 84:180 ("works Amenti
   Mobile", 402px) in Portfolio_2026.

   Same convention as the MOBILE-FIGMA block above: everything lives between
   this marker and the END marker and is appended last, so it overrides the
   earlier rules by source order and the whole pass can be reverted by
   deleting the block. Nothing above it was edited.
   A full pre-change copy is at css/styles.css.bak-before-cs-mobile.

   The frame stops after "Discovering and Defining the Vision", so the
   feature carousels, interview carousel, insight/journey/JTBD cards and the
   persona galleries have no design to match. Those are marked NOT IN FRAME
   below - they are stacked to stop them overflowing, not styled to a spec.
   ══════════════════════════════════════════════════════════════════════════ */

@media (max-width: 640px) {
  /* Figma's root auto-layout puts 64px between every top-level block, so the
     whole case-study rhythm follows from this one token: .case-study-section's
     margin-top, the divider/media/subsection calc()s and .subsections' gap are
     all already expressed in terms of it. */
  :root {
    --space-block-gap: var(--space-2xl);   /* 96px -> 64px */
  }

  /* Body copy drops 16/24 -> 14/22, scoped to the two case-study containers so
     the homepage keeps the type it was tuned to in the pass above. Everything
     inside inherits; only the headings and captions override. */
  #case-study-title,
  #case-study-sections {
    font-size: 0.875rem;    /* 14px */
    line-height: 1.5714;    /* 22px */
  }

  /* The 12rem right gutter is a desktop measure-limiter. On a 402 frame the
     content runs the full 354 between the 24px page gutters, so it goes to 0
     here - which also makes .media--full and .section-divider resolve to an
     exact 100vw, since both are written as calc()s around this token. */
  .case-study-section {
    --section-padding-right: 0rem;
  }

  /* Hero: 402x411 in the frame, flush under the 72px header. */
  #case-study-hero video,
  #case-study-hero img {
    aspect-ratio: 402 / 411;
  }

  /* Title block: 32px between every child, no extra top padding - the 64px
     margin-top already matches the frame's gap below the hero. */
  #case-study-title {
    padding-right: 0;
    padding-top: 0;
  }

  /* Written out rather than as --font-size-lg: the 1024 and 640 breakpoints
     above both shrink that token (down to 1.5rem here), so the token no longer
     means 32px at this width. The frame wants a literal 32/40. */
  #case-study-title h1 {
    font-size: 2rem;       /* 32px */
    line-height: 1.25;     /* 40px */
  }

  .case-study-meta {
    margin-top: var(--space-lg);   /* 32px, was 48 + 32 of padding */
    padding-block: 0;
  }

  /* Location/Duration and Team/Primary/Supporting stack instead of sitting in
     three columns; the 16px flex gap on .case-study-meta already matches, so
     the rows only need to become single-column at the same 16px. */
  .meta-row {
    grid-template-columns: 1fr;
  }

  /* Overview is the first section, and the frame gives it 32px below the meta
     block rather than the 64px that separates every later section. */
  #case-study-sections > .case-study-section:first-child {
    margin-top: var(--space-lg);
  }

  /* Both the section headings ("What was the problem?") and the big lead
     paragraphs (the overview and challenge statements) are one 22/28 style in
     the frame - and both are authored as `heading` in data.js, so a single
     rule covers them. */
  .case-study-section h2 {
    font-size: 1.375rem;   /* 22px */
    line-height: 1.2727;   /* 28px */
  }

  .subsection h3 {
    font-size: 1em;        /* inherits the 14px above */
    font-weight: 700;      /* the frame uses Ladna Bold, not semibold */
  }

  /* A blank line between paragraphs in the Figma text blocks is exactly one
     line-height, so the gap between paragraphs and between bullets is 22px,
     not the 32/24 the desktop layout uses. */
  .paragraph-group,
  .bullet-list {
    gap: 1.375rem;   /* 22px */
  }

  /* Inline media (the graph) runs the full 354 content width instead of the
     desktop 75% measure, and sits at the plain 32px section gap. Scoped to
     --inline on purpose: .media--full's width and max-height are calc()s of
     the same specificity, so widening .media itself would clobber them and
     collapse the full-bleed media back to the content width. */
  .media--inline {
    max-width: 100%;
    width: 100%;
  }

  .case-study-section > .media--inline {
    margin-top: 0;
  }

  /* 402x183 in the frame - a wider crop than the source video's own ratio, so
     the box is declared and the video covers it. */
  .media--full {
    aspect-ratio: 402 / 183;
    max-height: none;
  }

  /* Stat cards stack full width. 16px padding + 22px label + 8px + 28px value
     + 16px padding = the 90px the frame draws. */
  .stats-row {
    grid-template-columns: 1fr;
    gap: var(--space-sm);
  }

  .stat-card {
    padding-top: var(--space-sm);
  }

  .stat-card p {
    margin-bottom: 0.5rem;   /* 8px */
  }

  .stat-card span {
    font-size: 1.375rem;   /* 22px, matching h2 - not the 32px desktop size */
    line-height: 1.2727;
  }

  /* The chip grid wraps at 16px in both directions in the frame. */
  .chip-list {
    column-gap: var(--space-sm);
    row-gap: var(--space-sm);
  }

  /* The chips sit 32px under their paragraph, where the rest of the subsection
     runs at 24px, so the extra 8px is added here rather than widening the
     subsection's own gap. */
  .subsection > .chip-list {
    margin-top: 0.5rem;
  }

  .subsections--tight {
    gap: var(--space-md);   /* 24px */
  }

  /* Gallery: the two prototype shots stack full width with the same 6px
     between them, caption 12/20 underneath at 16px. */
  .gallery {
    flex-direction: column;
  }

  .gallery-image {
    width: 100%;
    height: auto;
  }

  .gallery-figure {
    display: flex;
    gap: var(--space-sm);
  }

  .gallery-caption {
    max-width: none;
    font-size: 0.75rem;   /* 12px */
    line-height: 1.6667;  /* 20px */
  }

  /* Together with #footer's 32px padding-top this makes the 64px the frame
     leaves between the last block and the footer text. */
  #case-study-sections {
    padding-bottom: var(--space-lg);
  }

  /* ─── NOT IN FRAME ────────────────────────────────────────────────────────
     The Figma frame ends before these exist. Left-to-right rows of flex:1
     children collapse to ~75px columns at this width, so they stack - which
     is what every other multi-column thing in the frame does - but this is a
     default to stop the overflow, not a design decision. Revisit when the
     frame covers them.
     ───────────────────────────────────────────────────────────────────────── */

  .insight-cards,
  .jtbd-items {
    flex-direction: column;
  }

  /* No Bigbank mobile frame exists yet - stacking is the assumption, since two
     504px screenshots side by side at 354 would be unreadable. */
  .image-grid {
    grid-template-columns: 1fr;
  }

  .image-grid--span-first > img:first-child {
    grid-row: auto;
    height: auto;
    object-fit: fill;
  }

  .journey-card {
    flex-basis: 100%;
  }

  .carousel-card {
    flex-direction: column;
  }

  .carousel-controls {
    gap: var(--space-lg);
  }
}
/* ══════════════════════════════════════════════════════════════════════════
   MOBILE-FIGMA-CS · 2026-08-18 · END
   ══════════════════════════════════════════════════════════════════════════ */


/* ══════════════════════════════════════════════════════════════════════════
   TABLET-FIGMA-CS · 2026-08-18 · START
   Case-study tablet pass, matched to Figma frame 84:530 ("works Amenti
   Tablet", 1024px) in Portfolio_2026.

   Written as a BAND (641-1024) rather than a max-width, because the two
   blocks above it are max-width and this one is appended last - a plain
   max-width:1024px here would cascade down and override the mobile pass.

   Revert: delete this block, plus the TABLET-FIGMA marker in js/render.js.
   Backups: css/styles.css.bak-before-cs-mobile, js/render.js.bak-before-tablet.

   The frame stops after "Results & Findings", so the carousels, feature
   carousels, insight/journey/JTBD cards have no tablet design - they are
   left on their desktop rules, which already fit at this width.
   ══════════════════════════════════════════════════════════════════════════ */

@media (min-width: 641px) and (max-width: 1024px) {
  /* Same 64px root rhythm as the mobile frame. Only case-study selectors read
     this token, so the homepage is unaffected. */
  :root {
    --space-block-gap: var(--space-2xl);   /* 96px -> 64px */
  }

  /* The whole point of the frame: even 64px gutters on both sides instead of
     the desktop's asymmetric 12rem right measure. --space-page is already 64
     at this width, so zeroing this token is all that's needed - and it makes
     .media--full and .section-divider resolve to an exact 1024. */
  .case-study-section {
    --section-padding-right: 0rem;
  }

  #case-study-title {
    padding-right: 0;
  }

  /* Type is the DESKTOP scale here, not the tablet token scale: the frame
     wants 64/60 and 32/40, but the @media (max-width: 1024px) block above
     shrinks --font-size-xl to 3rem and --font-size-lg to 1.75rem for the
     homepage. Writing literals keeps the case-study type at the frame's
     values without disturbing the homepage's tablet type. */
  #case-study-title h1 {
    font-size: 4rem;       /* 64px */
    line-height: 0.9375;   /* 60px */
  }

  .case-study-section h2 {
    font-size: 2rem;       /* 32px */
    line-height: 1.25;     /* 40px */
  }

  .subsection h3 {
    font-weight: 700;      /* the frame uses Ladna Bold, not semibold */
  }

  /* Hero is a 16:9 crop at this width (1024x576 in the frame). */
  #case-study-hero video,
  #case-study-hero img {
    aspect-ratio: 16 / 9;
  }

  /* Full-bleed media is a 1024x378 band. */
  .media--full {
    aspect-ratio: 1024 / 378;
    max-height: none;
  }

  /* The inline graph is inset from the content column: 768 of the 896. */
  .media--inline {
    max-width: 85.7143%;
  }

  /* The overview is the one section the frame gives 64px between its parts
     (lead statement -> body -> graph) where every later section uses 32.
     Its inline media then needs the default 32px top margin cancelled, or the
     graph would sit at 96. */
  #case-study-sections > .case-study-section:first-child {
    gap: var(--space-2xl);
  }

  #case-study-sections > .case-study-section:first-child > .media {
    margin-top: 0;
  }

  /* 16 + 24 label + 8 + 40 value + 16 = the 104px card the frame draws. */
  .stat-card {
    padding-top: var(--space-sm);
  }

  .stat-card p {
    margin-bottom: 0.5rem;   /* 8px */
  }

  .stat-card span {
    font-size: 2rem;       /* 32px - literal, see the note on h2 above */
    line-height: 1.25;     /* 40px */
  }

  /* Chips carry 24px side padding at this width, not 16. */
  .chip-list li {
    padding: 0 var(--space-md);
  }

  /* The chips sit 32px under their paragraph where the subsection runs at 24. */
  .subsection > .chip-list {
    margin-top: 0.5rem;
  }

  /* Gallery stays a row here (it stacks only on phones), 16px above the
     caption instead of 24. inline-flex has to become flex or the figure is
     shrink-to-fit and takes the images' max-content width. */
  .gallery-figure {
    display: flex;
    gap: var(--space-sm);
  }

  /* .gallery-caption sets a font-size but no line-height, so it inherits the
     base 1.5 and lands at 19.5px. The frame sets the 13px caption on a 24px
     line, which is what --line-height-sm is for. */
  .gallery-caption {
    line-height: var(--line-height-sm);
  }

  /* A row of images sharing one height must have widths proportional to each
     image's aspect ratio. flex-basis 0 + a per-image flex-grow does exactly
     that; --gallery-aspect is set from the real file by js/render.js, and the
     fallback of 1 just makes them equal until it arrives. This is also what
     stops the row overflowing - the old fixed height with width:auto could
     not shrink, so it ran to a constant 1001px at every viewport. */
  .gallery-image {
    flex-grow: var(--gallery-aspect, 1);
    flex-shrink: 1;
    flex-basis: 0;
    min-width: 0;
    width: auto;
    height: auto;
  }
}
/* ══════════════════════════════════════════════════════════════════════════
   TABLET-FIGMA-CS · 2026-08-18 · END
   ══════════════════════════════════════════════════════════════════════════ */


/* ══════════════════════════════════════════════════════════════════════════
   TABLET-FIGMA-HOME · 2026-08-20 · START
   Homepage tablet pass, matched to Figma frame 105:180 ("Homepage Tablet",
   1024px) in Portfolio_2026.

   Same convention as the blocks above: everything lives between this marker
   and the END marker and is appended last, so it overrides the earlier
   @media (max-width: 1024px) rules by source order rather than by editing
   them. Nothing above it was touched.

   The band is CLOSED (641-1024). The 1024 block above is max-width only, so
   an open block here would cascade straight into the mobile pass.

   What the frame changes, in one line: the tablet stops pretending to be a
   narrow desktop. Gutters were already 64, but the section padding was
   collapsed to 48 and the content was stacked into one column. The frame does
   the opposite - full 128px section rhythm, hero and both work columns side
   by side, three CV columns. Less padding relative to content, more content
   per screen.

   Type is set on the homepage's own selectors, NOT on --font-size-lg/xl.
   Those tokens are 1.75rem/3rem at this width and .stat-card span on the
   case-study pages reads --font-size-lg, so moving the token would silently
   resize the verified case-study tablet layout.
   ══════════════════════════════════════════════════════════════════════════ */

@media (min-width: 641px) and (max-width: 1024px) {

  /* ── Section rhythm ────────────────────────────────────────────────────
     Written out instead of via --space-section for the same reason as the
     type: that token is also read by the case-study pages at this width.
     Frame: 128 above and below the hero, 64 around works and the CV, and the
     works rows 128 apart. */
  #hero {
    padding-block: var(--space-3xl);   /* 128 */
    gap: var(--space-3xl);             /* 128 between text and portrait */
  }

  #works {
    padding-block: var(--space-2xl);   /* 64 */
    gap: var(--space-3xl);             /* 128 between the two work rows */
    /* The desktop formula subtracts --space-section, which no longer equals
       this section's padding here, so the term is written out to match. */
    scroll-margin-top: calc(var(--header-height) - var(--space-2xl));
  }

  #resume {
    padding-block: var(--space-2xl);   /* 64 */
  }

  /* ── Hero ──────────────────────────────────────────────────────────────
     Back to a row: 538 of text, 128 gap, 230 portrait = 896. The 1024 block
     above stacks it, which is what left the portrait stranded on its own
     line with a 500px measure of text above it. */
  #hero {
    flex-direction: row;
    align-items: center;
  }

  #hero .hero-text p,
  #hero .hero-text h1 {
    font-size: 2rem;        /* 32px */
    line-height: 1.25;      /* 40px */
  }

  /* One empty 40px line between the greeting and the statement, exactly as
     the frame's single text block sets it. Desktop's 64 is a desktop value. */
  #hero .hero-text p {
    margin-bottom: 2.5rem;  /* 40px */
  }

  /* ── Works ─────────────────────────────────────────────────────────────
     Bigbank and Flo Kasearu go back side by side: 406 + 84 + 406 = 896. The
     84px column gap is already the desktop value, so only the stacking
     override from the 1024 block needs undoing. */
  .works-row {
    grid-template-columns: 1fr 1fr;
  }

  .work-header h2 {
    font-size: 2rem;        /* 32px */
    line-height: 1.25;      /* 40px */
  }

  /* ── Contact ───────────────────────────────────────────────────────────
     The frame keeps the full 64px display size here; only mobile drops it. */
  #contact h2 {
    font-size: 4rem;        /* 64px */
    line-height: 0.9375;    /* 60px */
  }

  #contact p {
    margin-bottom: var(--space-md);   /* 24, not the desktop 32 */
  }

  /* ── CV ────────────────────────────────────────────────────────────────
     Three columns, as on desktop, but filling the 896 with no gap: the frame
     puts the entries at x 64 / 362.7 / 661.3, a 298.67 pitch, which is 896/3
     exactly. The rules underneath therefore meet and read as one continuous
     line. Longest entry text is 200px in a 298.67 column, so nothing crowds.

     (The component's 320px rule in the frame is a leftover from the desktop
     column width - it overhangs each column there. The entry positions are
     the reliable number, not the rule's width.) */
  .experience-grid {
    grid-template-columns: repeat(3, 1fr);
    column-gap: 0;
  }

  /* .experience-item h3 carries no font-size of its own, so it falls through
     to the UA's 1.17em and the whole entry renders at 18.72/28.08 - which is
     why each row came out 113px tall against the frame's 101. The frame wants
     plain 16/24. NOTE: this is true at every width, not just here; it is
     scoped to the tablet band so the verified desktop and mobile passes stay
     byte-identical until Marten decides to fix it there too. */
  .experience-item h3 {
    font-size: var(--font-size-base);   /* 16px */
    line-height: var(--line-height-base);
  }
}
/* ── Below the frame's width ────────────────────────────────────────────
   The frame is 1024 only. The hero's furniture is fixed - a 230 portrait
   plus the 128 gap plus 128 of gutters is 486px that never shrinks - so the
   text column is whatever is left: 538 at 1024, but 348 at 834 and 282 at
   768, which is iPad portrait and a very real size. That reads as a ribbon.

   So the row holds down to 900 (414 of text, still a sane measure) and the
   portrait drops below the text under that, the same move mobile makes. The
   band is closed at 641 so it cannot reach the mobile pass.

   Everything else - 128 section rhythm, 32px type, two work columns, three
   CV columns - is unchanged here; those all still fit. The CV is the next
   thing to give: it holds three clean columns to 768 and starts wrapping at
   about 700. Worth a frame if the sub-768 range ever matters.

   NOTE: this 900 threshold is the one number in this pass with no frame
   behind it. It is derived, not designed. */
@media (min-width: 641px) and (max-width: 899.98px) {
  #hero {
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-lg);   /* 32 stacked, not the row's 128 */
  }
}
/* ══════════════════════════════════════════════════════════════════════════
   TABLET-FIGMA-HOME · 2026-08-20 · END
   ══════════════════════════════════════════════════════════════════════════ */


/* ══════════════════════════════════════════════════════════════════════════
   FLUID-GUTTER · 2026-08-20 · START
   Desktop page gutter, interpolated between the two frames that exist.

   The problem this fixes: --space-page was a hard 256px, calibrated for the
   1728 desktop frame, and it never shrank. Since it is a padding and not a
   share of the width, the content column was "1728's leftovers" at every
   width - so it got NARROWER as the window narrowed:

       1728 -> 1216 content   (the frame, correct)
       1440 ->  928
       1253 ->  741
       1100 ->  588
       1025 ->  513   <- and the hero could not fit, so the PAGE OVERFLOWED
       1024 ->  896   <- one pixel later the tablet band takes over and it
                         jumps back up, wider than it was at 1300

   The page scrolled horizontally everywhere from 1327 down to 1025.

   The fix: two real anchor points, both taken from Figma frames, linearly
   interpolated. 64 at 1024 (frame 105:180) and 256 at 1728 (frame 17:682).
   Slope is 192/704 = 3/11, which is why the term reads the way it does -
   "64px at 1024, plus three elevenths of every pixel beyond that."

       1024 ->  64.0   (matches the tablet band exactly, so 1024/1025 is now
                        continuous instead of a 383px jump)
       1253 -> 126.4
       1440 -> 177.5
       1728 -> 256.0   (the frame, unchanged)
       1728+->  256    clamped; --content-max-width: 76rem then centres it

   NOTE this is NOT what rem does. A rem is fixed against the root font size
   and does not know the viewport exists - 16rem and 256px resize identically,
   which is to say not at all. Fluid spacing needs a vw term, and clamp() to
   stop it running away at the extremes.

   Band is min-width 1025 so it cannot reach the tablet or mobile passes,
   both of which set --space-page themselves anyway.
   ══════════════════════════════════════════════════════════════════════════ */

@media (min-width: 1025px) {
  :root {
    --space-page: clamp(4rem, calc((100vw - 1024px) * 3 / 11 + 4rem), 16rem);
  }

  /* Both frames align the nav with the content column - 256 at 1728, 64 at
     1024. The header was pinned to a flat --space-2xl instead, so on desktop
     the logo sat at 64 while the hero text started at 256. At tablet the two
     values happened to be equal, which is why it only ever looked wrong on
     desktop. Reading the same token as everything else fixes it at every
     width. (Mobile sets this itself, so it is untouched there.)

     REVERT this one rule alone if you want the logo back on the far edge. */
  .site-header {
    padding-inline: var(--space-page);
  }

  /* The hover panel is a fixed 460px card, absolutely positioned at left:0 of
     its column. In the RIGHTMOST column that runs 460px rightward from a
     column that only has ~280 of room, so it hangs off the right of the page.
     A visibility:hidden absolute element still contributes to scrollWidth, so
     this forced a horizontal scrollbar even with nothing hovered - it is what
     was really scrolling the page from 1327 down to 1025, and it spills into
     the right gutter even at the full 1728 frame.

     Pre-existing, not caused by the fluid gutter - but the wider content
     column moves the third column further right, so it would have started
     biting at 1440 too. The last column's panel anchors to its RIGHT edge and
     opens leftward instead.

     Tablet is unaffected either way: the 1024 block gives the panel
     width:100%, and mobile makes it static inside the accordion. */
  .experience-item:nth-child(3n) .experience-panel {
    left: auto;
    right: 0;
  }

  /* Same story as the panel: .gallery-image is `height: 15.5rem; width: auto`,
     which is a row that CANNOT shrink - it sits at a constant 1001px at every
     viewport. That is 97.8% of the 1024 content column at the 1728 frame, so
     it was calibrated there and quietly spilled into the right gutter at every
     narrower width. It never triggered a scrollbar before only because the
     old 256 gutter plus the 12rem right measure left 448px of slack to spill
     into; the fluid gutter removes that slack, so it started scrolling the
     page at 1025-1090.

     The fix is the one the tablet pass already proved - flex-basis 0 plus a
     per-image flex-grow equal to its own aspect ratio, which js/render.js
     publishes as --gallery-aspect. Plain flex-shrink does NOT work: shrink is
     proportional to basis and both files are ~3530px wide natively, so they
     end up equal-width and ragged-bottomed.

     The row now fills the column at every width. At 1728 that is 1024 instead
     of 1001 - a 2.3% difference, which is the only visual change on desktop.

     inline-flex must become flex or the figure is shrink-to-fit and takes the
     images' max-content width, defeating the whole thing. */
  .gallery-figure {
    display: flex;
  }

  .gallery-image {
    flex-grow: var(--gallery-aspect, 1);
    flex-shrink: 1;
    flex-basis: 0;
    min-width: 0;
    width: auto;
    height: auto;
  }
}
/* ══════════════════════════════════════════════════════════════════════════
   FLUID-GUTTER · 2026-08-20 · END
   ══════════════════════════════════════════════════════════════════════════ */


/* ══════════════════════════════════════════════════════════════════════════
   TITLE-GAP · 2026-08-20 · START
   Hero -> h1 spacing on the case studies, corrected to the frames.

   All three desktop frames agree exactly: the Title block sits 96 below the
   hero band and carries 32 of its own top padding, putting the h1 at 128.
   Amenti 69:183, Bigbank 95:183 and Flo 105:845 all read pad=32 with a 96
   gap above. The CSS was producing 96 to the h1 - margin-top 64 + the same
   padding 32 - so every case study ran 32px tight.

   DESKTOP ONLY, deliberately. The other two bands are already correct against
   their own frames and must not move:
     - mobile (<=640) zeroes the padding and takes the gap from the 64px
       margin alone, which is what the 402 frame wants;
     - tablet (641-1024) overrides neither, so it is already 64 + 32 = 96,
       which is what frame 84:530 wants.
   Changing the base rule would have shifted both.
   ══════════════════════════════════════════════════════════════════════════ */

@media (min-width: 1025px) {
  #case-study-title {
    /* 96. There is no 96px space token - --space-3xl is 128 - but
       --space-block-gap is exactly this: the rhythm between top-level
       case-study blocks, which is what this gap is. It resolves to 96 here and
       is only reduced to 64 inside the tablet and mobile bands, which this
       rule does not reach. */
    margin-top: var(--space-block-gap);
  }
}
/* ══════════════════════════════════════════════════════════════════════════
   TITLE-GAP · 2026-08-20 · END
   ══════════════════════════════════════════════════════════════════════════ */


/* ══════════════════════════════════════════════════════════════════════════
   EXPERIENCE-TYPE · 2026-08-20 · START
   CV entry, re-matched to the updated Experience/Default component (25:3).

   Marten dropped the role and the year to a smaller size. The component now
   reads, top to bottom, with NO gaps between the three lines:

       company    16/24  bold      y  0 -> 24
       role       14/22  regular   y 24 -> 46
       location   14/22  regular   y 46 -> 68
       rule       1px, 16 below    y 84
       total                          85   (was 101)

   The dots circle also went 20 -> 18, its three inner dots spanning 10.3
   across at 1.9 each.

   APPLIED AT EVERY WIDTH, at Marten's request. Note that in Figma the mobile
   entries in frame 80:181 are DETACHED frames rather than instances, so they
   did not inherit the component edit and still read 16/24 with 20px dots -
   he is updating them to match.

   This also finally fixes the long-standing h3 bug: .experience-item h3
   carries no font-size, so it fell through to the UA's 1.17em and the entry
   rendered at 18.72/28.08 instead of 16/24. It was patched inside the tablet
   band during the tablet pass; setting it here covers desktop and mobile too,
   and the tablet band's copy is now a harmless duplicate of the same value.
   At mobile this means the company line drops 18.72 -> 16, which is what the
   mobile frame asked for all along.
   ══════════════════════════════════════════════════════════════════════════ */

/* 16/24. Without this the h3 takes the UA's 1.17em and every line below
   inherits from it. */
.experience-item h3 {
  font-size: var(--font-size-base);
  line-height: var(--line-height-base);
}

.experience-role,
.experience-location {
  font-size: 0.875rem;    /* 14px */
  line-height: 1.5714;    /* 22px */
}

.experience-dots {
  width: 18px;
  height: 18px;
}

/* Three 1.9px dots spanning 10.3 total, so the outer two sit at +/-4.2. */
.experience-dots::before {
  width: 1.9px;
  height: 1.9px;
  box-shadow: 4.2px 0 0 var(--color-muted), -4.2px 0 0 var(--color-muted);
}

/* The component stacks the three lines flush - 24 + 22 + 22 = 68 with no
   gaps - where both of these were --space-3xs (4px). ::after keeps its own
   16px margin, which is the Line frame's padding-top: 68 + 16 + 1 = 85.

   Held to >=641 deliberately. The mobile pass sets its own 2px trigger gap and
   6px rule margin, tuned to frame 80:181 and checked on a real phone; those
   are spacing decisions, not part of the type change, so they stay. Say the
   word if the mobile entry should go flush like the component. */
@media (min-width: 641px) {
  .experience-trigger {
    gap: 0;
  }

  .experience-item {
    gap: 0;
  }
}
/* ══════════════════════════════════════════════════════════════════════════
   EXPERIENCE-TYPE · 2026-08-20 · END
   ══════════════════════════════════════════════════════════════════════════ */


/* ══════════════════════════════════════════════════════════════════════════
   CARDS-MOBILE · 2026-08-20 · START
   Interview card and JTBD card at mobile, matched to two new frames:
   InterviewCard-Mobile (110:371) and JTBD-Card-Mobile (110:349).

   These are the two components the mobile case-study pass explicitly parked -
   its comment reads "a default to stop the overflow, not a design decision.
   Revisit when the frame covers them." The frames now cover them, so this is
   that revisit. Appended rather than edited into the MOBILE-FIGMA-CS block, so
   that block stays intact as shipped and this one reverts on its own.

   Both frames are drawn 530 wide rather than 402, so the type sizes and the
   column widths in them are illustrative - what they specify is the GROUPING
   and the spacing, which is what is taken here. Body type is left on the
   mobile pass's own 14/22 scale.
   ══════════════════════════════════════════════════════════════════════════ */

@media (max-width: 640px) {
  /* Interview card. Stacking already came from the mobile pass; the frame's
     change is the gap between the person block and the quote, which was
     inheriting the desktop --space-lg (32) and reads as a hole between two
     halves of one card. */
  .carousel-card {
    gap: 17px;
  }

  /* JTBD. Was a 4-high stack - correct, but very tall. The frame groups the
     four into 2x2, which is the whole point: same content, roughly half the
     height, and Actor/Situation and Motivation/Outcome pair up as the two
     halves of the sentence they actually form.

     display: grid is what retires the mobile pass's `flex-direction: column`
     on this element - flex-direction simply stops applying, so that rule is
     left untouched above rather than edited out. */
  .jtbd-items {
    display: grid;
    grid-template-columns: 1fr 1fr;
    column-gap: 0.5rem;          /* 8 */
    row-gap: var(--space-sm);    /* 16 */
  }

  .jtbd-card {
    padding: var(--space-sm);    /* 16, from the desktop 24 */
    gap: 0.75rem;                /* 12 between the heading and the grid */
  }

  /* The frame pulls the text 2px into the label's line box. CSS gap cannot go
     negative, so this is 0 rather than -2 - a 2px difference that the frame's
     16/24 + 13/19 type produced and the 14/22 mobile scale would not match
     anyway. */
  .jtbd-item {
    gap: 0;
  }
}
/* ══════════════════════════════════════════════════════════════════════════
   CARDS-MOBILE · 2026-08-20 · END
   ══════════════════════════════════════════════════════════════════════════ */

/* ══════════════════════════════════════════════════════════════════════════
   NOT-FOUND · 2026-08-21 · START
   Styles for 404.html. They live here rather than in a <style> block on the
   page itself because the CSP in _headers has no 'unsafe-inline' in
   style-src, which would silently drop an inline block.
   REVERT: delete this block and 404.html together.
   ══════════════════════════════════════════════════════════════════════════ */
.notfound {
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: flex-start;
  gap: var(--space-md);
}

.notfound__code {
  margin: 0;
  font-size: var(--font-size-xl);
  line-height: var(--line-height-xl);
  font-weight: 700;
}

.notfound__text {
  margin: 0;
  font-size: var(--font-size-lg);
  line-height: var(--line-height-lg);
}

.notfound__link {
  font-size: var(--font-size-base);
  color: var(--color-text);
}
/* ══════════════════════════════════════════════════════════════════════════
   NOT-FOUND · 2026-08-21 · END
   ══════════════════════════════════════════════════════════════════════════ */
