/* ============================================
   DESIGN TOKENS
   ============================================ */
:root {
  --color-primary: #1d3f8f;
  --color-primary-dark: #142c66;
  --color-primary-darker: #0e2049;
  --color-accent: #f5a623;
  --color-accent-dark: #d98e12;
  --color-bg: #fffdf9;
  --color-surface: #ffffff;
  --color-text: #2b3550;
  --color-text-muted: #5b6b85;
  --color-border: #e7ecf5;
  --color-on-primary: #ffffff;
  --color-on-accent: #17223f;

  --font-heading: "Poppins", sans-serif;
  --font-body: "Inter", sans-serif;

  --space-2xs: 8px;
  --space-xs: 12px;
  --space-sm: 16px;
  --space-md: 24px;
  --space-lg: 40px;
  --space-xl: 64px;
  --space-2xl: 96px;

  --radius-sm: 10px;
  --radius-md: 18px;
  --radius-lg: 28px;
  --radius-pill: 999px;

  --shadow-sm: 0 2px 8px rgba(20, 44, 102, 0.08);
  --shadow-md: 0 12px 32px rgba(20, 44, 102, 0.14);
  --shadow-lg: 0 24px 60px rgba(20, 44, 102, 0.18);

  --duration-fast: 150ms;
  --duration-base: 250ms;
  --duration-slow: 400ms;
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);

  --container-max: 1240px;

  /* ---- Golden-ratio type scale -------------------------------------
     Every step is φ times the one below it, so the sizes are related by
     a single constant rather than picked individually. Anchored on the
     body size, which stays fluid — the whole scale scales with it.
       body    20.0px
       lead    32.3px   (body × φ)
       display 52.3px   (body × φ²)
     φ also sets the body leading and the paragraph rhythm below, so the
     vertical spacing comes from the same ratio as the sizes. */
  --phi: 1.618;
  --text-body: clamp(1rem, 1.55vw, 1.25rem);
  --text-lead: calc(var(--text-body) * var(--phi));
  --text-display: calc(var(--text-body) * var(--phi) * var(--phi));
  /* One body line — the natural unit for spacing between paragraphs. */
  --rhythm: calc(var(--text-body) * var(--phi));

  /* ---- Responsive column grid --------------------------------------
     4 columns on mobile, 8 on tablet, 12 on desktop (see the breakpoint
     overrides below). --grid-col is the computed width of a single
     column, which lets non-grid elements (e.g. the carousel's flex
     cards) be sized in exact column units so they share the same rhythm.
     Span of N columns = N*--grid-col + (N-1)*--grid-gutter. */
  --grid-columns: 4;
  --grid-gutter: var(--space-sm);
  --grid-col: calc(
    (100% - (var(--grid-columns) - 1) * var(--grid-gutter)) / var(--grid-columns)
  );
}

@media (min-width: 768px) {
  :root {
    --grid-columns: 8;
    --grid-gutter: var(--space-md);
  }
}

@media (min-width: 1024px) {
  :root {
    --grid-columns: 12;
    --grid-gutter: var(--space-md);
  }
}

/* ============================================
   RESET
   ============================================ */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
body, h1, h2, h3, p, figure, ul { margin: 0; }
ul { padding: 0; list-style: none; }
img { max-width: 100%; display: block; }
a { color: inherit; text-decoration: none; }
button { font: inherit; color: inherit; background: none; border: 0; padding: 0; }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
    /* Must also zero the DELAY, not just the duration: the page-load
       entrance holds elements at opacity 0 during their stagger delay
       (animation-fill-mode: both), so without this these users would
       wait on invisible content for up to ~0.6s. */
    animation-delay: 0ms !important;
    transition-delay: 0ms !important;
  }
}

body {
  font-family: var(--font-body);
  color: var(--color-text);
  background-color: var(--color-bg);
  /* Full-page grid-and-clouds backdrop. It is a transparent PNG, so the
     background-color above still supplies the page tint. Stretched to the
     document's own height (100% 100%) rather than tiled — the clouds sit at
     fixed spots in the artwork and would visibly repeat otherwise. */
  background-image: url("../assets/site-background.png");
  background-repeat: no-repeat;
  background-position: top center;
  background-size: 100% 100%;
  font-size: 16px;
  line-height: 1.6;
  overflow-x: hidden;
}

.wrap {
  max-width: var(--container-max);
  margin-inline: auto;
  padding-inline: var(--space-sm);
  display: grid;
  grid-template-columns: repeat(var(--grid-columns), minmax(0, 1fr));
  column-gap: var(--grid-gutter);
  /* Vertical rhythm stays with each component's own margins, so rows are
     not gapped here. */
  row-gap: 0;
}

/* Anything not explicitly placed occupies the full measure, so adding the
   grid does not reflow content that was previously full-width blocks. */
.wrap > * {
  grid-column: 1 / -1;
  min-width: 0;
}

/* A grid item stretches to fill its span by default, which would blow a
   pill button out to the full column width. Buttons keep their intrinsic
   size and sit centred in the span instead. */
.wrap > .btn {
  justify-self: center;
}

/* Focus visibility */
a:focus-visible,
button:focus-visible,
[tabindex]:focus-visible {
  outline: 3px solid var(--color-accent);
  outline-offset: 3px;
  border-radius: 4px;
}

/* Visually hidden but still announced by screen readers and indexed. Used
   for form labels and for wording that only exists inside artwork. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  background: var(--color-primary);
  color: #fff;
  padding: 12px 20px;
  border-radius: 0 0 8px 0;
  z-index: 1000;
}
.skip-link:focus {
  left: 0;
}

/* ============================================
   TYPOGRAPHY
   ============================================ */
h1, h2, h3 {
  font-family: var(--font-heading);
  font-weight: 800;
  color: var(--color-primary);
  line-height: 1.1;
}

.section-title {
  font-size: clamp(2rem, 4vw + 1rem, 3rem);
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: var(--space-sm);
  margin-bottom: var(--space-md);
}

.squiggle-underline {
  width: 90px;
  height: 12px;
  color: var(--color-accent);
  flex-shrink: 0;
}

/* ============================================
   BUTTONS
   ============================================ */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 48px;
  padding: 0 32px;
  border-radius: var(--radius-pill);
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: 0.95rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  cursor: pointer;
  transition: transform var(--duration-fast) var(--ease-out),
              box-shadow var(--duration-fast) var(--ease-out),
              background-color var(--duration-fast) var(--ease-out),
              color var(--duration-fast) var(--ease-out);
}

.btn-primary {
  background-color: var(--color-accent);
  color: var(--color-on-accent);
  box-shadow: var(--shadow-sm);
}
/* Inverts to white with blue text on hover. The outline is an inset shadow
   rather than a real border so the button's box does not change size and
   shift the layout mid-hover. */
.btn-primary:hover,
.btn-primary:focus-visible {
  background-color: #ffffff;
  color: var(--color-primary);
  box-shadow: inset 0 0 0 2px var(--color-primary), var(--shadow-md);
  transform: translateY(-2px);
}
.btn-primary:active {
  transform: translateY(0);
}

/* ============================================
   HEADER / NAV
   ============================================ */
.site-header {
  position: sticky;
  top: 0;
  z-index: 100;
  /* Fully opaque so the page's background artwork does not show through the
     bar (it was translucent + blurred before, which let the grid and clouds
     read behind the nav). */
  background: var(--color-bg);
  border-bottom: 1px solid var(--color-border);
}

/* Inherits the column grid from .wrap rather than setting its own flex. */
.header-inner {
  align-items: center;
  min-height: 76px;
}
/* All three are pinned to row 1. Without an explicit grid-row, the nav (which
   spans every column at desktop so it can centre on the page) overlaps the
   logo's columns, and auto-placement pushes it onto a second row — which left
   the logo sitting above the links instead of beside them. */
.header-inner .logo { grid-column: 1 / span 3; grid-row: 1; }
/* Last column, hard right. */
.nav-toggle { grid-column: -2 / -1; grid-row: 1; justify-self: end; }
.primary-nav { grid-row: 1; }

.logo-img {
  height: 40px;
  width: auto;
}

.nav-toggle {
  display: none;
  flex-direction: column;
  justify-content: center;
  gap: 5px;
  width: 44px;
  height: 44px;
  cursor: pointer;
  border-radius: var(--radius-sm);
}
.nav-toggle-bar {
  display: block;
  width: 24px;
  height: 3px;
  background: var(--color-primary);
  border-radius: 2px;
  transition: transform var(--duration-base) var(--ease-out), opacity var(--duration-base) var(--ease-out);
  margin-inline: auto;
}
.nav-toggle[aria-expanded="true"] .nav-toggle-bar:nth-child(1) { transform: translateY(8px) rotate(45deg); }
.nav-toggle[aria-expanded="true"] .nav-toggle-bar:nth-child(2) { opacity: 0; }
.nav-toggle[aria-expanded="true"] .nav-toggle-bar:nth-child(3) { transform: translateY(-8px) rotate(-45deg); }

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

.nav-link {
  font-family: var(--font-heading);
  font-weight: 600;
  font-size: 0.95rem;
  color: var(--color-primary);
  position: relative;
  padding: 8px 2px;
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  transition: color var(--duration-fast) var(--ease-out);
}
.nav-link::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: 4px;
  height: 2px;
  width: 0%;
  background: var(--color-accent);
  transition: width var(--duration-base) var(--ease-out);
}
.nav-link:hover::after,
.nav-link.is-active::after {
  width: 100%;
}
.nav-link:hover {
  color: var(--color-accent-dark);
}

/* ============================================
   HERO
   ============================================ */
.hero {
  position: relative;
  padding-top: var(--space-xl);
  padding-bottom: var(--space-2xl);
  overflow: hidden;
}

.hero-decor { position: absolute; inset: 0; pointer-events: none; z-index: 0; overflow: visible; }
/* height:auto keeps each exported asset at its own aspect ratio — only the
   width is specified below. */
.decor { position: absolute; height: auto; }

/* Offsets are read off the design, expressed against the stage (whose box is
   the photo's own span). The top asterisk stays above the title band, which
   starts at 24%, so it cannot land on the "D" of Designer. */
.decor-asterisk--top   { top: 13%;   right: 34%;   width: 40px; }
.decor-triangle        { top: 54.4%; right: 13.6%; width: 22px; }
.decor-squares         { top: 68.3%; right: 16%;   width: 64px; }
.decor-ring            { top: 61.5%; right: 11.5%; width: 28px; }
.decor-dot             { top: 64.5%; right: 9.2%;  width: 30px; }
.decor-asterisk--small { top: 58%;   right: 7.4%;  width: 30px; }
.decor-arrow           { top: 70.4%; right: 23.6%; width: 67px; }

/* Every offset above is a percentage of the stage, and on mobile the stage is
   the whole stacked column (photo + title + intro + button) rather than just
   the photo — so those percentages resolve onto the portrait and the copy.
   The cluster is a desktop composition; hide it wholesale below the tablet
   breakpoint instead of letting shapes land on Jules' face. */
@media (max-width: 767px) {
  .hero-decor { display: none; }
}

/* Mobile: plain stack (photo, title, intro, button). The overlay
   composition from the design takes over at the desktop breakpoint. */
.hero-stage {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: var(--space-md);
}

.hero-title {
  font-size: clamp(2.2rem, 9vw, 3.4rem);
  font-style: italic;
  font-weight: 800;
  display: grid;
  grid-template-columns: 1fr;
  width: 100%;
  text-align: center;
  row-gap: var(--space-md);
}

.hero-image-wrap {
  position: relative;
  width: min(92vw, 460px);
  /* Matches hero-image-trim.png exactly, so the photo fills this box with no
     letterboxing. That makes the box equal the visible person, which is what
     every percentage offset in the hero is measured against. */
  aspect-ratio: 960 / 999;
  margin-inline: auto;
  order: -1;
}

.hero-blob {
  position: absolute;
  /* Real artwork (assets/circle.png): a true circle with its own built-in
     top-to-bottom gradient fade, so no CSS shape/gradient hack is needed
     anymore. Sized as a square and centered dead-center behind the photo. */
  width: 84%;
  aspect-ratio: 1 / 1;
  left: 50%;
  top: 17%;
  transform: translateX(-50%);
  background-image: url("../assets/circle.png");
  background-size: 100% 100%;
  background-repeat: no-repeat;
  z-index: 0;
}

.hero-image {
  position: relative;
  z-index: 1;
  /* Fills its wrapper exactly — no oversize/negative-margin offset — so the
     photo and the circle behind it share the same center. Size the photo
     via .hero-image-wrap's width instead. */
  width: 100%;
  height: 100%;
  object-fit: contain;
  object-position: bottom center;
  /* No fade mask here — the artwork carries its own gradient, and masking on
     top of it double-faded the lower half of the photo. */
}

.hero-greeting {
  font-size: clamp(1.5rem, 2vw + 1rem, 2rem);
  margin-bottom: var(--space-xs);
}

.hero-text {
  color: var(--color-text-muted);
  max-width: 46ch;
  font-size: 0.98rem;
}

.hero-cta {
  align-self: flex-start;
  min-height: 52px;
  padding-inline: 38px;
  font-size: 0.98rem;
}

/* ============================================
   WORKS
   ============================================ */
.works {
  padding-block: var(--space-2xl);
  position: relative;
}

.works-title {
  display: block;
  text-align: center;
  width: 100%;
  margin-inline: auto;
  margin-bottom: var(--space-lg);
}

.works-carousel {
  width: 100%;
}

.works-track {
  display: flex;
  /* Same gutter as the page grid so the cards line up with its columns. */
  gap: var(--grid-gutter);
  overflow-x: auto;
  /* proximity, not mandatory. The last page scrolls to the track's true end,
     which is not always on a card boundary — mandatory snapping would yank it
     to the nearest card the moment the glide finished, showing as a jolt.
     proximity still snaps manual swipes but leaves that landing alone. */
  scroll-snap-type: x proximity;
  scroll-behavior: smooth;
  padding-bottom: var(--space-sm);
  scrollbar-width: none;
}
.works-track::-webkit-scrollbar { display: none; }

.work-card {
  scroll-snap-align: start;
  /* Card widths are ~35% narrower than the full-bleed originals
     (82/45/25% -> 53/29/16.25%) across all three breakpoints. */
  /* Exactly 2 of the 4 mobile columns, so the carousel sits on the same
     grid as the rest of the page (3 of 8 at tablet, 3 of 12 at desktop —
     i.e. four cards filling the row). */
  flex: 0 0 calc(var(--grid-col) * 2 + var(--grid-gutter));
  border-radius: var(--radius-lg);
  overflow: hidden;
  box-shadow: var(--shadow-sm);
  background: var(--color-surface);
  transition: transform var(--duration-base) var(--ease-out), box-shadow var(--duration-base) var(--ease-out);
}
.work-card:hover,
.work-card:focus-within {
  transform: translateY(-6px);
  box-shadow: var(--shadow-md);
}

/* The whole card is the trigger for its project dialog. A real <button> so it
   is keyboard reachable and announced as activatable. */
.work-open {
  display: block;
  width: 100%;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  text-align: inherit;
  font: inherit;
  color: inherit;
}

.work-card figure { position: relative; margin: 0; }
.work-card img {
  width: 100%;
  /* height:auto is required, not optional: the <img> tags carry height
     attributes (for CLS reservation), which act as presentational hints
     and make the height definite — that would override aspect-ratio and
     render each card at the image's full natural height. */
  height: auto;
  aspect-ratio: 3 / 4;
  object-fit: cover;
}
.work-card figcaption {
  position: absolute;
  inset: auto 0 0 0;
  padding: var(--space-md) var(--space-sm) var(--space-sm);
  background: linear-gradient(to top, rgba(14, 32, 73, 0.88), transparent);
  color: #fff;
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: 1rem;
  opacity: 0;
  transform: translateY(8px);
  transition: opacity var(--duration-base) var(--ease-out), transform var(--duration-base) var(--ease-out);
}
.work-card:hover figcaption,
.work-card:focus-within figcaption {
  opacity: 1;
  transform: translateY(0);
}
.work-card figcaption span {
  display: block;
  font-family: var(--font-body);
  font-weight: 400;
  font-size: 0.8rem;
  color: #cdd9f5;
  margin-top: 2px;
}

/* Three columns so the dots sit centred on the section while the arrow
   pair stays flush with the left edge of the first card, per the design. */
.works-controls {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  margin-top: var(--space-lg);
}
.works-arrows {
  grid-column: 1;
  justify-self: start;
  display: flex;
  gap: var(--space-2xs);
}
.works-dots {
  grid-column: 2;
}

.carousel-arrow {
  width: 44px;
  height: 44px;
  border-radius: 50%;
  border: 2px solid var(--color-primary);
  color: var(--color-primary);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: background-color var(--duration-fast) var(--ease-out), color var(--duration-fast) var(--ease-out);
}
.carousel-arrow svg { width: 20px; height: 20px; }
/* A true 4px stroke on screen. The markup's stroke-width is in viewBox units
   (24) but the icon renders at 20px, so a plain stroke-width would be scaled
   down — non-scaling-stroke pins it to device pixels instead. */
.carousel-arrow svg path {
  stroke-width: 4;
  vector-effect: non-scaling-stroke;
}
.carousel-arrow:hover {
  background-color: var(--color-primary);
  color: #fff;
}
.carousel-arrow:disabled {
  opacity: 0.35;
  cursor: not-allowed;
}
.carousel-arrow:disabled:hover {
  background-color: transparent;
  color: var(--color-primary);
}

.works-dots {
  display: flex;
  gap: var(--space-2xs);
}
.work-dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  border: 2px solid var(--color-primary);
  background: transparent;
  cursor: pointer;
  padding: 0;
  transition: background-color var(--duration-fast) var(--ease-out);
}
.work-dot.is-active {
  background-color: var(--color-primary);
}

/* ============================================
   WORK DETAIL DIALOG
   ============================================ */
.work-modal {
  position: fixed;
  inset: 0;
  z-index: 1000;
  display: grid;
  place-items: center;
  padding: var(--space-sm);
}
.work-modal[hidden] { display: none; }

/* Closed state. The dialog is un-hidden first, then .is-open is added on the
   next frame so these values are the transition's starting point — without
   that the panel would simply appear. Pointer events are off while closing
   so the fading overlay does not swallow clicks. */
.work-modal { pointer-events: none; }
.work-modal.is-open { pointer-events: auto; }

.work-modal-scrim {
  position: absolute;
  inset: 0;
  /* Strong enough to isolate the panel from the page behind it. */
  background: rgba(14, 32, 73, 0.55);
  opacity: 0;
  transition: opacity var(--duration-base) var(--ease-out);
}
.work-modal.is-open .work-modal-scrim { opacity: 1; }

.work-modal-panel {
  position: relative;
  width: min(100%, 1040px);
  max-height: 90vh;
  overflow-y: auto;
  background: var(--color-surface);
  border: 4px solid var(--color-primary);
  border-radius: 26px;
  padding: clamp(var(--space-md), 3vw, var(--space-xl));
  box-shadow: var(--shadow-lg);
  /* Eases up and in rather than appearing. Slightly slower than the scrim so
     the panel reads as arriving on top of it. */
  opacity: 0;
  transform: scale(0.96) translateY(10px);
  transition: opacity var(--duration-slow) var(--ease-out),
              transform var(--duration-slow) var(--ease-out);
}
.work-modal.is-open .work-modal-panel {
  opacity: 1;
  transform: scale(1) translateY(0);
}

.work-modal-close {
  position: absolute;
  top: 14px;
  right: 14px;
  width: 44px;
  height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  color: var(--color-primary);
  cursor: pointer;
  transition: background-color var(--duration-fast) var(--ease-out),
              color var(--duration-fast) var(--ease-out);
}
.work-modal-close svg { width: 22px; height: 22px; }
.work-modal-close:hover {
  background: var(--color-primary);
  color: #fff;
}

.work-modal-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-lg);
  align-items: start;
}

.work-modal-number {
  display: flex;
  align-items: center;
  gap: 6px;
  font-family: var(--font-heading);
  font-weight: 800;
  font-size: clamp(2.4rem, 5vw, 4rem);
  line-height: 1;
  color: var(--color-primary);
  margin-bottom: var(--space-md);
}
.work-modal-asterisk {
  width: clamp(28px, 4vw, 48px);
  height: auto;
  margin-left: 4px;
}

.work-modal-programs {
  display: flex;
  gap: var(--space-2xs);
  margin-bottom: var(--space-md);
}
.work-modal-programs img {
  width: clamp(38px, 4vw, 52px);
  height: auto;
  border-radius: 10px;
}

.work-modal-title {
  font-family: var(--font-heading);
  font-weight: 800;
  font-size: clamp(1.5rem, 3vw, 2.4rem);
  line-height: 1.1;
  color: var(--color-primary);
  margin-bottom: var(--space-sm);
}

.work-modal-desc {
  color: var(--color-primary);
  font-weight: 600;
  font-size: clamp(0.9rem, 1.1vw, 1rem);
  line-height: 1.55;
}

/* Centres the artwork in its cell both ways. align-self overrides the grid's
   align-items:start, which was pinning the image to the top of the row and
   leaving it off-centre against the taller text column. */
.work-modal-media {
  align-self: center;
  display: flex;
  justify-content: center;
}

.work-modal-media img {
  width: 100%;
  height: auto;
  /* Keeps tall artwork inside the panel instead of pushing it past the fold. */
  max-height: 68vh;
  object-fit: contain;
  border-radius: 8px;
  display: block;
}

@media (min-width: 768px) {
  .work-modal-grid {
    /* Text column left, artwork right, as laid out in the overview design. */
    grid-template-columns: 0.85fr 1fr;
    gap: var(--space-xl);
  }
}

/* ============================================
   ABOUT
   ============================================ */
.about {
  padding-top: var(--space-2xl);
  /* The dot grid is absolutely positioned and hangs ~120px below the photo,
     so it eats this section's bottom padding — which left the contact section
     starting only ~40px after it, against ~148px between works and about.
     The extra allowance evens that rhythm out. Added here rather than as
     padding on .cta because the contact decor is positioned as a percentage
     of that section's height, and growing it would drag the sparkles out of
     alignment with the heading. */
  padding-bottom: calc(var(--space-2xl) + 108px);
}

/* Uses .wrap's column grid. On mobile both halves span all 4 columns and
   simply stack; the split is introduced at the tablet breakpoint. */
.about-grid {
  align-items: center;
  row-gap: var(--space-2xl);
}

.about-media {
  position: relative;
  width: min(100%, 420px);
  margin-inline: auto;
}

/* Narrower and shorter than the photo, peeking out top-left, as drawn. */
.about-panel {
  position: absolute;
  top: -11%;
  left: -11%;
  width: 69%;
  height: 97%;
  background: var(--color-primary);
  z-index: 0;
}

.about-photo {
  position: relative;
  z-index: 1;
  width: 100%;
  /* height:auto is required: the <img> carries a height attribute, which is a
     presentational hint that makes the height definite and would otherwise
     beat aspect-ratio — rendering the photo 420x670 instead of near-square. */
  height: auto;
  aspect-ratio: 700 / 670;
  object-fit: cover;
}

/* Orange ring + bullseye, straddling the panel's top-right corner. */
.about-orbits {
  position: absolute;
  top: -5%;
  left: 55%;
  z-index: 2;
  display: flex;
  align-items: center;
  gap: 6px;
}
.about-orbits img { display: block; height: auto; }
.about-orbits img:first-child { width: 26px; }
.about-orbits img:last-child  { width: 30px; }

/* Dot pattern asset, right-aligned under the photo. */
.about-dotgrid {
  position: absolute;
  right: 0;
  bottom: -22%;
  width: 51%;
  height: auto;
  z-index: 2;
}

.about-content {
  text-align: left;
  position: relative;
}

/* Three stacked zigzag rules sitting to the right of the heading block.
   Hidden below the two-column breakpoint, where there is no room beside
   the heading and they would collide with the text. */
/* Flex row rather than absolute offsets: align-items:flex-end pins the
   zigzag block's baseline to the bottom of "Hello I'm Jules" automatically,
   so it stays aligned as the clamped heading sizes change with viewport. */
.about-headings {
  display: flex;
  align-items: flex-end;
  gap: var(--space-sm);
  margin-bottom: var(--space-md);
}
.about-headings-text { flex: 0 1 auto; }

.about-zigzag {
  display: none;
  flex: 0 0 auto;
  width: min(34%, 190px);
  /* No margin-left:auto — that shoved the block to the far edge of the
     column. In the design it butts right up against the headings, separated
     only by the flex gap. */
  pointer-events: none;
}
/* Uses the exported zigzag asset at its natural aspect ratio (height:auto),
   so the teeth keep their drawn angle. The previous inline SVGs stretched a
   shared <pattern> with preserveAspectRatio="none", which skewed the rows
   against each other and squashed them into one another. */
.about-zigzag img {
  display: block;
  width: 100%;
  height: auto;
  margin-bottom: 10px;
}
.about-zigzag img:last-child { margin-bottom: 0; }

/* "About Me" and "Hello I'm Jules" read as one tightly-set two-line block in
   the design, so the heading's default section spacing is collapsed here. */
#about-heading {
  line-height: 1;
  margin-bottom: 2px;
  /* body × φ² — the top step of the golden-ratio scale. */
  font-size: var(--text-display);
}

.about-subheading {
  font-family: var(--font-heading);
  font-weight: 800;
  /* body × φ — the middle step. */
  font-size: var(--text-lead);
  line-height: 1.05;
  color: var(--color-primary);
  /* Spacing below now lives on .about-headings — keeping it here would sit
     inside the flex item and push the zigzag's flex-end alignment down. */
  margin-bottom: 0;
}

.about-text {
  color: var(--color-primary);
  /* 500, not 600. Semi-bold suited the original three-line blurb, but across
     eleven lines it set as a solid slab and flattened the contrast with the
     headings above (which are 800). */
  font-weight: 500;
  /* The anchor the rest of the scale is derived from. */
  font-size: var(--text-body);
  /* Leading is φ itself. */
  line-height: var(--phi);
  max-width: 56ch;
}
/* Space between stacked paragraphs; the last one keeps the section's own
   bottom spacing rather than adding to it. */
.about-text + .about-text {
  margin-top: var(--rhythm);
  /* Slightly more open than the first paragraph's φ leading. */
  line-height: 1.78;
}

/* ============================================
   CTA / CONTACT
   ============================================ */
.cta {
  position: relative;
  padding-block: var(--space-2xl);
  text-align: center;
  overflow: hidden;
}

.cta-decor { position: absolute; inset: 0; pointer-events: none; }

/* Sparkle placement is read off the design. Each is positioned by its CENTRE
   (left/top = centre, then translated back by half) so the numbers map
   directly to the artwork's coordinates instead of needing corner maths. */
.decor-star {
  color: var(--color-primary);
  transform: translate(-50%, -50%);
}
.decor-star--1 { left: 18.3%; top: 16%;   width: 38px; height: 38px; }
.decor-star--2 { left: 18.8%; top: 36%;   width: 32px; height: 32px; }
.decor-star--3 { left: 68%;   top: 13%;   width: 54px; height: 54px; }
.decor-star--4 { left: 84.3%; top: 16%;   width: 32px; height: 32px; }
.decor-star--5 { left: 61.9%; top: 35%;   width: 35px; height: 35px; }
.decor-star--6 { left: 38.5%; top: 17.5%; width: 18px; height: 18px; }
.decor-star--7 { left: 42.4%; top: 17%;   width: 28px; height: 28px; }

/* The looping arrow left of the button — the same exported asset used in the
   hero, not a hand-drawn path. */
/* left/top set the arrow's CENTRE, since it is translated by -50%. The
   arrowhead is not at the centre though — it sits at the very bottom-right of
   the artwork (99.6% across, 94.1% down), so centring the image on the button
   left the tip pointing well below it. These values place the TIP level with
   the button's label and ~16px to its left. */
.decor-cta-arrow {
  left: 40.4%;
  top: 72.6%;
  width: min(5.4%, 68px);
  height: auto;
  transform: translate(-50%, -50%);
}

/* Flat oval ringing just the heading. Positioned against .cta-inner so it
   stays locked to the heading regardless of how the copy below reflows. */
.cta-ellipse {
  position: absolute;
  top: -22px;
  left: 50%;
  /* Tilts down to the right, as drawn (the previous -5deg leaned the wrong
     way). */
  transform: translateX(-50%) rotate(9deg);
  width: min(82%, 520px);
  height: auto;
  color: var(--color-primary);
  pointer-events: none;
}

.cta-inner {
  position: relative;
  z-index: 1;
  /* Must clear the heading's single-line width PLUS .wrap's 40px side padding
     — at 720px the content box was only 640px and the heading wrapped. */
  max-width: 800px;
}

/* Larger than the shared .section-title cap — in the design the heading spans
   just over half the section width. */
/* Sized so the full string sits on ONE line inside .cta-inner's content box.
   Larger values silently wrap "Mind?" onto a second line. */
#cta-heading {
  /* body × φ² — the same top step used by the About heading, so both
     sections read off one scale instead of two unrelated sets of sizes. */
  font-size: var(--text-display);
  white-space: nowrap;
  /* .section-title is inline-flex, but as a grid item that blockifies to
     flex — it then fills the whole span with its text packed at flex-start,
     so the parent's text-align:center no longer centres it. Sizing it to its
     content and centring it in the span restores that. */
  justify-self: center;
}

/* Nudges the contact button down, toward the arrow's head. */
.cta-inner .btn { margin-top: 26px; }

.cta-text {
  color: var(--color-primary);
  font-weight: 700;
  /* The scale's anchor, exactly φ² below the heading above. */
  font-size: var(--text-body);
  line-height: var(--phi);
  /* Justified so the block sets as a clean rectangle rather than ragging on
     both sides. hyphens:auto is doing the real work — forcing flush edges
     without it opens uneven word gaps (rivers) down the block. Deliberately
     not combined with text-wrap:balance: balance shortens lines to even them
     out, which leaves justify more space to stretch, not less. */
  text-align: justify;
  -webkit-hyphens: auto;
  hyphens: auto;
  max-width: 42ch;
  margin-inline: auto;
  /* Extra breathing room under the heading. This also carries the button
     down with it, since both sit in normal flow below. */
  margin-top: var(--space-md);
  margin-bottom: var(--space-lg);
}

/* ============================================
   FOOTER
   ============================================ */
.site-footer {
  background: var(--color-primary-darker);
  color: #fff;
  padding-block: var(--space-lg);
}

/* Uses .wrap's grid; each row spans the full measure and is centred. */
.footer-inner {
  justify-items: center;
  row-gap: var(--space-md);
  text-align: center;
}

.logo--footer {
  padding-bottom: var(--space-sm);
  border-bottom: 1px solid rgba(255, 255, 255, 0.15);
  width: 100%;
  display: flex;
  justify-content: center;
}

/* Uses the dedicated white logo asset (pencil stacked over the wordmark),
   not the blue header logo pushed through a brightness/invert filter. */
.logo-img--footer {
  height: 58px;
  width: auto;
}

.footer-nav ul {
  display: flex;
  gap: var(--space-lg);
  flex-wrap: wrap;
  justify-content: center;
}

.footer-nav .nav-link {
  color: #fff;
}
.footer-nav .nav-link::after {
  background: var(--color-accent);
}

.footer-copy {
  font-size: 0.85rem;
  color: rgba(255, 255, 255, 0.65);
}

/* ============================================
   SCROLL-REVEAL ANIMATION
   ============================================ */
.reveal {
  opacity: 0;
  transform: translateY(24px);
  transition: opacity var(--duration-slow) var(--ease-out), transform var(--duration-slow) var(--ease-out);
}
.reveal.is-visible {
  opacity: 1;
  transform: translateY(0);
}

/* The works cascade in rather than all appearing at once. ~70ms apart reads
   as one connected movement; much more and the last card lags noticeably
   behind the first. The global prefers-reduced-motion rule zeroes both the
   duration and these delays. */
.works-track .work-card:nth-child(1) { transition-delay: 0ms; }
.works-track .work-card:nth-child(2) { transition-delay: 70ms; }
.works-track .work-card:nth-child(3) { transition-delay: 140ms; }
.works-track .work-card:nth-child(4) { transition-delay: 210ms; }
.works-track .work-card:nth-child(5) { transition-delay: 280ms; }
.works-track .work-card:nth-child(6) { transition-delay: 350ms; }
.works-track .work-card:nth-child(7) { transition-delay: 420ms; }
.works-track .work-card:nth-child(8) { transition-delay: 490ms; }

/* The heading leads the cards in. */
.works-title { transition-delay: 0ms; }

/* ============================================
   PAGE-LOAD ENTRANCE
   ============================================
   Pure CSS (no JS trigger) so it starts the moment the hero paints, with
   no flash of unstyled content. Each element animates a property it does
   not otherwise set — the title WORDS move, not .hero-title itself, since
   that carries a translateY at tablet+ that an animation would clobber.
   The global prefers-reduced-motion rule collapses these to ~0s. */
@keyframes enter-fade {
  from { opacity: 0; }
  to   { opacity: 1; }
}
@keyframes enter-up {
  from { opacity: 0; transform: translateY(26px); }
  to   { opacity: 1; transform: translateY(0); }
}
@keyframes enter-down {
  from { opacity: 0; transform: translateY(-16px); }
  to   { opacity: 1; transform: translateY(0); }
}
@keyframes enter-left {
  from { opacity: 0; transform: translateX(-44px); }
  to   { opacity: 1; transform: translateX(0); }
}
@keyframes enter-right {
  from { opacity: 0; transform: translateX(44px); }
  to   { opacity: 1; transform: translateX(0); }
}
@keyframes enter-scale {
  from { opacity: 0; transform: scale(0.92); }
  to   { opacity: 1; transform: scale(1); }
}

.site-header,
.hero-image-wrap,
.hero-title-word--left,
.hero-title-word--right,
.hero-decor,
.hero-intro,
.hero-cta {
  animation-duration: 620ms;
  animation-timing-function: var(--ease-out);
  /* "both" holds the from-state during the delay, so nothing flickers
     into view before its turn in the stagger. */
  animation-fill-mode: both;
}

/* Staggered so the eye lands on the photo first, then the name, then the
   supporting copy — roughly 60-100ms apart. */
.site-header            { animation-name: enter-down;  animation-delay: 0ms; }
.hero-image-wrap        { animation-name: enter-scale; animation-delay: 120ms; }
.hero-title-word--left  { animation-name: enter-left;  animation-delay: 260ms; }
.hero-title-word--right { animation-name: enter-right; animation-delay: 330ms; }
.hero-intro             { animation-name: enter-up;    animation-delay: 430ms; }
.hero-cta               { animation-name: enter-up;    animation-delay: 510ms; }
.hero-decor {
  animation-name: enter-fade;
  animation-delay: 640ms;
  animation-duration: 900ms;
}

/* ============================================
   RESPONSIVE — TABLET (>=768px)
   ============================================ */
@media (min-width: 768px) {
  .wrap { padding-inline: var(--space-lg); }

  /* Tablet: title overlaps the photo (row 1), but the intro and button sit
     in a normal row beneath it. There isn't enough width here to float them
     beside the photo without squeezing the paragraph to an unreadable size,
     so the full overlay is deferred to the desktop breakpoint. */
  .hero-stage {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    row-gap: var(--space-md);
  }
  .hero-title,
  .hero-image-wrap,
  .hero-decor {
    grid-column: 1 / -1;
    grid-row: 1;
  }

  .hero-title {
    display: flex;
    justify-content: space-between;
    align-items: center;
    width: 100%;
    text-align: initial;
    row-gap: 0;
    z-index: 1;
    transform: translateY(-13%);
    font-size: clamp(1.6rem, 4.6vw, 3.1rem);
  }

  .hero-image-wrap {
    justify-self: center;
    align-self: center;
    width: min(38vw, 400px);
    order: 0;
    /* Above the title so the head and shoulders occlude the words. */
    z-index: 2;
  }

  .hero-intro { grid-column: 1; grid-row: 2; }
  .hero-cta   { grid-column: 2; grid-row: 2; align-self: end; }

  /* The photo takes a larger share of the row at this width, so the design's
     offsets would drop the asterisk onto the hair (and, moved sideways, onto
     the "D"). Tucked above the title line here; the desktop breakpoint
     restores the drawn position. */
  .decor-asterisk--top { top: 1%; right: 24%; }

  .work-card { flex: 0 0 calc(var(--grid-col) * 3 + var(--grid-gutter) * 2); }

  .about-grid {
    text-align: left;
  }
  /* Asymmetric, as drawn: the text column is wider than the photo column.
     An even split left the heading row too narrow and wrapped "Hello I'm
     Jules" once the zigzag block took its share. 3/8 and 5/8 at tablet;
     5/12 and 7/12 at desktop (see below). */
  .about-media   { grid-column: 1 / span 3; }
  .about-content { grid-column: 4 / -1; }
  /* Only room for these once the heading has its own column. */
  .about-zigzag { display: block; }
}

/* ============================================
   RESPONSIVE — DESKTOP (>=1024px)
   ============================================ */
@media (min-width: 1024px) {
  .nav-toggle { display: none; }

  /* The nav spans the full grid and centres its links, so the group sits on
     the header's midpoint. Confining it to columns 4-12 (i.e. the space left
     over by the logo) pushed its centre well right of the page centre, which
     read as misaligned. The logo occupies the same row in columns 1-3, but
     the centred group stays clear of it at every desktop width. */
  .primary-nav { grid-column: 1 / -1; }
  .primary-nav ul {
    justify-content: center;
    gap: clamp(48px, 7vw, 110px);
  }

  .about-media   { grid-column: 1 / span 5; }
  .about-content { grid-column: 6 / -1; }

  /* Desktop: the full composition from the design. The photo alone stays in
     normal flow and sets the section height; the title, intro and button are
     layered over it, so all four share the photo's vertical span rather than
     stacking into separate bands. Inset values use the wrap's own padding so
     they line up with the content column (an absolutely positioned child is
     laid out against the padding box, which ignores that padding). */
  .hero-stage { display: block; }

  /* Photo ~35% of the viewport width, matching the design. It is nudged just
     right of dead centre, as drawn — that asymmetry is what opens up enough
     room on the left for the "c" of Graphic to clear the hair. Uses `left`
     rather than a transform, which the load animation already owns. */
  .hero-image-wrap {
    width: min(35.5vw, 460px);
    margin-inline: auto;
    left: 1.8%;
  }

  /* Vertically centred on the face, where the silhouette narrows to just the
     head — that is what lets both words sit close in without the "c" or the
     "D" disappearing behind the hair or shoulders. */
  /* Insets are deliberately asymmetric: in the design "Graphic" is set in
     further from the left edge than "Designer" is from the right. */
  .hero-title {
    position: absolute;
    top: 27%;
    left: 12%;
    right: 7%;
    width: auto;
    transform: none;
    font-size: clamp(2.1rem, 5.5vw, 4.4rem);
  }

  .hero-intro {
    position: absolute;
    top: 60%;
    left: var(--space-lg);
    width: min(30%, 360px);
    z-index: 3;
  }
  .hero-greeting { font-size: clamp(1.15rem, 1.9vw, 1.75rem); }
  /* Raised from ~0.8rem: against a 76px headline the copy was reading as
     fine print, and the scale jump between them was jarring. */
  .hero-text { font-size: clamp(0.9rem, 1.15vw, 1.05rem); max-width: none; }

  .hero-cta {
    position: absolute;
    top: 93%;
    right: var(--space-lg);
    z-index: 3;
  }

  /* Back to the design's offsets (the tablet rule above relocates it). */
  .decor-asterisk--top { top: 13%; right: 34%; }

  /* 3 of 12 columns each, so four cards fill the row edge-to-edge. */
  .work-card { flex: 0 0 calc(var(--grid-col) * 3 + var(--grid-gutter) * 2); }
}

/* ============================================
   RESPONSIVE — MOBILE NAV (<1024px)
   ============================================ */
@media (max-width: 1023px) {
  .nav-toggle {
    display: inline-flex;
  }

  .primary-nav {
    position: fixed;
    inset: 76px 0 0 0;
    background: var(--color-bg);
    transform: translateY(-12px);
    opacity: 0;
    visibility: hidden;
    transition: opacity var(--duration-base) var(--ease-out), transform var(--duration-base) var(--ease-out), visibility var(--duration-base);
    padding: var(--space-lg);
    z-index: 99;
  }
  .primary-nav.is-open {
    opacity: 1;
    visibility: visible;
    transform: translateY(0);
  }
  .primary-nav ul {
    flex-direction: column;
    gap: var(--space-md);
  }
  .primary-nav .nav-link {
    font-size: 1.4rem;
  }
}

/* ============================================
   RESPONSIVE — LARGE DESKTOP (>=1440px)
   ============================================ */
@media (min-width: 1440px) {
  body { background-size: 2600px auto; }
}

/* ============================================
   RESPONSIVE — MOBILE TEXT ALIGNMENT (<768px)
   ============================================
   Placed last on purpose: these override base rules (.hero-cta's
   align-self, .about-content's text-align) that are declared further up
   the file. At equal specificity the later rule wins, so an earlier
   media query would have been silently ignored. */
@media (max-width: 767px) {
  /* Single-column mobile: the hero and about copy read as centred blocks
     rather than left-aligned text sitting under a centred photo. The
     contact section is already centred at every width. */
  .hero-intro,
  .about-content,
  .about-headings-text { text-align: center; }

  /* These carry a max-width, so they need centring within their column too
     — text-align alone would leave the measure hugging the left edge. */
  .hero-text,
  .about-text { margin-inline: auto; }

  /* Body copy is justified so each paragraph reads as a clean rectangular
     block instead of ragging on both edges. hyphens:auto is doing real work
     here — on a measure this narrow, justifying without it opens ugly gaps
     between words (rivers). It relies on <html lang="en">, which is set. */
  .hero-text,
  .about-text {
    text-align: justify;
    -webkit-hyphens: auto;
    hyphens: auto;
  }

  .hero-cta { align-self: center; }

  /* The heading block is a flex row (it pairs with the zigzag at wider
     sizes); with the zigzag hidden here, centre what remains. */
  .about-headings { justify-content: center; }

  /* More air above the contact button so it sits clear of the copy. */
  .cta-text { margin-bottom: var(--space-2xl); }

  /* The sparkles and looping arrow are scattered by percentage across the
     section; at this width those positions land on the heading, the copy and
     the button. There is no room to rearrange them cleanly, so the cluster
     is dropped here. The ellipse lives in .cta-inner, not .cta-decor, so it
     survives — crossing the heading is intentional, not a collision. */
  .cta-decor { display: none; }
}

/* ============================================
   CONTACT PAGE
   ============================================ */
:root {
  /* Sampled from the supplied artwork so the live form matches it exactly. */
  --field-fill: #e2e2e2;
  --submit-blue: #1f4490;
}

.contact-banner-section {
  padding-top: var(--space-lg);
  padding-bottom: var(--space-xl);
}
.contact-banner {
  width: 100%;
  height: auto;
}

.contact-form-section {
  position: relative;
  padding-bottom: var(--space-2xl);
}
.contact-form-wrap {
  position: relative;
}

/* "Get In Touch" with an asterisk asset either side. */
.get-in-touch {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: clamp(var(--space-md), 6vw, var(--space-2xl));
  font-family: var(--font-heading);
  font-weight: 800;
  font-size: clamp(1.5rem, 3.2vw, 2.4rem);
  color: var(--color-primary);
  margin-bottom: var(--space-lg);
}
.get-in-touch .git-star {
  width: clamp(24px, 3vw, 40px);
  height: auto;
}

.contact-form {
  position: relative;
  z-index: 1;
  /* width:100% is load-bearing. As a grid item, auto inline margins cancel
     the default stretch, leaving the form to shrink-to-fit its content —
     which collapsed it to roughly a quarter of the measure. A definite
     width restores the intended size, capped by max-width and centred by
     the auto margins. */
  width: 100%;
  max-width: 660px;
  margin-inline: auto;
}
.form-row { margin: 0 0 var(--space-md); }

.field {
  display: block;
  width: 100%;
  background: var(--field-fill);
  border: 2px solid transparent;
  border-radius: var(--radius-sm);
  padding: 0 var(--space-md);
  /* 52px keeps the tap target comfortably above the 44px minimum. */
  min-height: 52px;
  font-family: var(--font-body);
  font-size: 1rem;
  color: var(--color-text);
  transition: border-color var(--duration-fast) var(--ease-out),
              background-color var(--duration-fast) var(--ease-out);
}
.field::placeholder { color: #7c7c7c; }
.field:hover { background: #dcdcdc; }
.field:focus {
  outline: none;
  background: #ebebeb;
  border-color: var(--color-primary);
}
.field--message {
  min-height: 260px;
  padding-block: var(--space-sm);
  resize: vertical;
  line-height: 1.5;
}

.form-actions {
  margin: var(--space-lg) 0 0;
  text-align: center;
}
.btn-submit {
  background: var(--submit-blue);
  color: #fff;
  border-radius: var(--radius-sm);
  min-height: 52px;
  padding-inline: var(--space-2xl);
  box-shadow: var(--shadow-sm);
}
.btn-submit:hover,
.btn-submit:focus-visible {
  background: #fff;
  color: var(--submit-blue);
  box-shadow: inset 0 0 0 2px var(--submit-blue), var(--shadow-md);
  transform: translateY(-2px);
}
.btn-submit:active { transform: translateY(0); }

/* Scattered marks around the form, positioned against .contact-form-wrap. */
.form-decor {
  position: absolute;
  inset: 0;
  pointer-events: none;
  color: var(--color-primary);
}
.fdecor { position: absolute; }
.fdecor-plus--1   { top: 30%; left: 4%;   width: 30px; height: 30px; }
.fdecor-plus--2   { top: 50%; right: 4%;  width: 34px; height: 34px; }
.fdecor-target--1 { top: 25%; right: 7%;  width: 32px; height: 32px; }
.fdecor-target--2 { top: 47%; left: 7%;   width: 32px; height: 32px; }
.fdecor-x--1      { top: 62%; right: 9%;  width: 20px; height: 20px; }
.fdecor-x--2      { top: 84%; left: 9%;   width: 20px; height: 20px; }

/* The marks sit in the margins beside the form; below the two-column
   breakpoint there is no margin left to hold them without landing on the
   fields, so they are dropped rather than allowed to collide. */
@media (max-width: 767px) {
  .form-decor { display: none; }
}

/* Contact form submit feedback */
.form-status {
  margin: var(--space-md) 0 0;
  text-align: center;
  font-weight: 600;
  font-size: 0.95rem;
  min-height: 1.4em;           /* reserves space so the layout does not jump */
}
.form-status:empty { display: none; }
.form-status.is-pending { color: var(--color-text-muted); }
.form-status.is-success { color: #1a7f4b; }   /* 4.9:1 on the page background */
.form-status.is-error   { color: #b3261e; }   /* 5.6:1 on the page background */
