/* Page-level layout for the site. Load after tokens.css and components/bundle.css.
   Components and type come from the design system; this file only places sections on the page. */

/* Sections sit space-64 apart on phones, space-96 on tablets and space-128 on desktop (from Figma: Desktop - 26). */
:root { --section-gap: var(--space-64); }
@media (min-width: 768px) {
  :root { --section-gap: var(--space-96); }
}
@media (min-width: 1024px) {
  :root { --section-gap: var(--space-128); }
}

/* The page's container: the design system's .container, at most content-max plus its 40px desktop margins (1600px) and centred on wider
   screens. Every section sits in it, apart from the header and the contact band, which span the whole window. Sizes that scale with the
   page use --page-width instead of 100vw, so they stop growing at 1600px too. Sections whose Figma design runs edge to edge (the statement,
   About) cap their width at --container-max without the container's margins. */
:root {
  --container-max: calc(var(--content-max) + 2 * var(--margin-desktop));
  --page-width: min(100vw, var(--container-max));
}

.site-main {
  display: flex;
  flex-direction: column;
  gap: var(--section-gap);
}

/* Read by screen readers, not shown: for words a link or heading needs that the design leaves out. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ---------- Availability note (from Figma: nav, node 284:701, "notifcation" 394:1278) ----------
   "Now available for work" beside the logo, header-gap (40px) after it as in Figma. The icon is Figma's 25px ring and 9px dot,
   drawn in currentColor so it follows the theme and turns white in the menu: the dot throbs while a copy of the ring pings outward.
   It stays in the header while it fits beside the Menu button; on narrower screens it moves into the open menu, beside the logo. */
.availability {
  display: flex;
  align-items: center;
  gap: var(--space-16);
  font-family: var(--font-body);
  font-size: 16px;
  font-weight: 400;
  line-height: 27px;
  letter-spacing: -0.03em;
  white-space: nowrap;
}
/* In the header it sits with the logo, and pushes the links and buttons to the right. */
.site-header > .availability { margin-inline-end: auto; }

.availability__icon {
  box-sizing: border-box;
  position: relative;
  flex: none;
  width: 25px;
  height: 25px;
  border: 1px solid currentColor;
  border-radius: 50%;
}
.availability__icon::before,
.availability__icon::after {
  content: "";
  position: absolute;
  border-radius: 50%;
  animation-duration: 1.5s;
  animation-iteration-count: infinite;
}
/* The ping: a copy of the ring that grows outward and fades. */
.availability__icon::before {
  inset: -1px;
  border: 1px solid currentColor;
  opacity: 0;
  animation-name: availability-ping;
  animation-timing-function: cubic-bezier(0, 0, 0.2, 1);
}
/* The dot: swells and brightens as the ping sets off, then softens. */
.availability__icon::after {
  top: 7px;
  left: 7px;
  width: 9px;
  height: 9px;
  background: currentColor;
  animation-name: availability-throb;
  animation-timing-function: ease-in-out;
}
@keyframes availability-ping {
  from { transform: scale(1); opacity: 0.6; }
  to { transform: scale(2); opacity: 0; }
}
@keyframes availability-throb {
  0%, 100% { transform: scale(1); opacity: 0.5; }
  20% { transform: scale(1.3); opacity: 1; }
}

/* The menu's copy: beside the white logo, centred on it, fading in and out with the logo and the X (timings from bundle.css). */
.menu__availability {
  display: none;
  position: absolute;
  top: calc(var(--space-24) + (83px - 27px) / 2);
  left: calc(var(--menu-inset) + 28px + var(--header-gap));
  opacity: 0;
  transition: opacity 200ms ease-in;
}
.menu.is-open .menu__availability { opacity: 1; transition: opacity 350ms ease-out 450ms; }

/* Too narrow to sit beside the Menu button with header-gap (40px) between them: show it in the menu instead.
   Phone padding 24 + logo 28 + gap 40 + note 211 + gap 40 + Menu button 115 + padding 24 = 483px. */
@media (max-width: 482px) {
  .site-header > .availability { display: none; }
  .menu__availability { display: flex; }
}

@media (prefers-reduced-motion: reduce) {
  .availability__icon::before,
  .availability__icon::after { animation: none; }
  .availability__icon::after { opacity: 1; }
  .menu__availability { transition: none !important; }
}

/* ---------- Menu: LinkedIn (from Figma: menu open, node 284:761, group 692:6960) ----------
   The LinkedIn icon and "nicholahudson" in Montserrat 16px uppercase (-0.03em, on a 14px line), 8px apart, in the menu's white.
   From 768px it sits bottom left, lined up with the logo: menu-inset from the left and space-32 from the bottom. On phones the links
   start near the left edge, so it would read as a fifth link there: instead it sits under them, centred, space-64 below the last one.
   space-8 of padding above and below makes it a 45px tap target, taken off the offsets so the icon sits where measured. It fades in
   and out with the logo and the X (timings from bundle.css), and opens in a new tab, so the menu stays open behind it. */
.menu__linkedin {
  position: absolute;
  bottom: calc(var(--space-32) - var(--space-8));
  left: var(--menu-inset);
  display: flex;
  align-items: center;
  gap: var(--space-8);
  padding-block: var(--space-8);
  color: inherit;
  font-family: var(--font-body);
  font-size: 16px;
  font-weight: 400;
  line-height: 14px;
  letter-spacing: -0.03em;
  text-transform: uppercase;
  text-decoration-line: none;
  white-space: nowrap;
  opacity: 0;
  transition: opacity 200ms ease-in;
}
.menu__linkedin img { display: block; }
.menu__linkedin:hover .menu__linkedin-text {
  text-decoration-line: underline;
  text-decoration-thickness: var(--underline-width);
}
.menu.is-open .menu__linkedin { opacity: 1; transition: opacity 350ms ease-out 450ms; }

/* Phones: the menu stacks the links and LinkedIn in a column, centred as a group. Replaces the Menu component's own centring of the
   links (bundle.css: margin auto on .menu__nav), which would otherwise push LinkedIn to the bottom. */
@media (max-width: 767px) {
  .menu { flex-direction: column; }
  .menu__nav { margin: auto auto 0; }
  .menu__linkedin {
    position: static;
    margin: calc(var(--space-64) - var(--space-8)) auto auto;
  }
}
@media (prefers-reduced-motion: reduce) {
  .menu__linkedin { transition: none !important; }
}

/* ---------- Header entrance ----------
   On page load the header's parts drop into place from above as they fade in, one after another from the logo to Contact me (or the
   Menu button): the opposite of the rest of the page, which rises into place. site.js numbers the parts showing at this width
   (--nav-order), adds .is-entered once Montserrat is in, and holds the statement back until the last part has landed (about 0.9s on
   desktop). backwards, not both, so nothing is left transformed afterwards. Only under .js; with reduced motion the header is simply there. */
.site-header {
  --nav-ease: cubic-bezier(0.2, 0.7, 0.2, 1);
  --nav-drop: 600ms;
  --nav-stagger: 60ms;
}
@media (prefers-reduced-motion: no-preference) {
  .js .site-header:not(.is-entered) :is(.site-header__home, .site-header__links > li, .site-header__actions > .button, .site-header__menu-button),
  .js .site-header:not(.is-entered) > .availability { opacity: 0; }
  .site-header.is-entered :is(.site-header__home, .site-header__links > li, .site-header__actions > .button, .site-header__menu-button),
  .site-header.is-entered > .availability {
    animation: nav-drop-in var(--nav-drop) var(--nav-ease) calc(var(--nav-order, 0) * var(--nav-stagger)) backwards;
  }
}
@keyframes nav-drop-in {
  from { opacity: 0; transform: translateY(calc(-1 * var(--space-48))); }
  to { opacity: 1; transform: none; }
}

/* ---------- Floating menu button (from Figma: Frame 33, node 508:6319) ----------
   The logo Button, fixed space-32 above the bottom of the screen, in the middle. site.js adds .is-shown once the page
   has scrolled half a screen. It slides up from below the screen and back down, with the menu's timings
   (in: 450ms with a soft landing, out: 300ms ease-in). Hidden, it can't be clicked or tabbed to. */
.menu-dock {
  position: fixed;
  z-index: 800; /* under the menu (900), which covers it while open */
  bottom: var(--space-32);
  left: 50%;
  display: flex;
  transform: translate(-50%, calc(100% + var(--space-32)));
  visibility: hidden;
  transition: transform 300ms ease-in, visibility 0s linear 300ms;
}
.menu-dock.is-shown {
  transform: translate(-50%, 0);
  visibility: visible;
  transition: transform 450ms cubic-bezier(0.2, 0.7, 0.2, 1);
}
@media (prefers-reduced-motion: reduce) {
  .menu-dock,
  .menu-dock.is-shown { transition: none; }
}

/* ---------- Statement (from Figma: Final > Frame 47, node 445:2562) ----------
   "Nichola Hudson." with a stem and leaves growing from the N, leaves on the s, and the small intro text. Built and tuned in
   prototypes/leaf-heading.html, which has the integration notes and the Figma measurements behind the numbers here.
   At 1440 wide: "Nichola" 497px Italiana on a 402.4px line with 208px above it for the leaves (the N's tall leaf reaches 32.65px higher);
   "Hudson." 365px on a 359.4px line, 8px up into the line above and right-aligned 40px from the right; both -4% tracking. Everything is
   sized from --statement-size (Nichola's font size), so the composition scales as one.
   It always spans the full width of the page: it scales with it (497 / 1440 of the width), up to the 1600px container, then centres.
   The real tablet and phone sizes aren't designed yet.
   Clipped sideways: Nichola's last letter has some empty space after it, which could reach past the edge where a scrollbar takes up room. */
.statement {
  --statement-size: calc(var(--page-width) * 497 / 1440);
  --statement-pad: calc(var(--statement-size) * 208 / 497); /* room above Nichola, where its leaves grow */
  position: relative;
  width: 100%;
  max-width: var(--container-max);
  margin-inline: auto;
  margin-top: var(--section-gap); /* the gap under the header, like the gaps between sections */
  padding-top: var(--statement-pad);
  overflow-x: clip;
}

/* From 1024 up, on screens tall enough, the first screen shows Nichola (and the small text beside it) and nothing else: the bottom of its
   letters sits 24px above the bottom of the screen, and Hudson. starts below the fold.
   - On tall screens the extra height goes above Nichola. On shorter screens the gap above the leaves closes up to show more of the word,
     but the leaves never come closer than 24px to the header: the statement starts 24px below it, and Nichola's line starts at least the
     tall leaf's height (240.651px of Figma's 497) below that. On screens shorter still, Nichola runs past the fold, so it still spans the
     full width.
   - --statement-top is how far down the page the statement starts: the header (24px padding, the 83px logo, 24px padding) plus that 24px.
   - 0.74383em is how far Nichola's baseline sits below the top of its line. */
@media (min-width: 1024px) {
  .statement {
    --statement-top: calc(var(--space-24) * 3 + 83px);
    --statement-fold: calc(100svh - var(--statement-top) - var(--space-24)); /* where Nichola's baseline goes */
    --statement-pad: max(var(--statement-fold) - var(--statement-size) * 0.74383, var(--statement-size) * 240.651 / 497);
    margin-top: var(--space-24);
  }
}

.statement__name {
  font-family: var(--font-display);
  font-weight: 400;
  white-space: nowrap;
}
.statement__line { display: block; }
/* Tracking goes on the words: an em letter-spacing is worked out where it's declared, so on the h1 it would use the h1's size. */
.statement__word { display: block; letter-spacing: -0.04em; }

.statement__word--first {
  font-size: var(--statement-size);
  line-height: calc(402.4 / 497);
}
/* In Figma Hudson.'s frame is 341px tall, 18.4px less than its line, and the next section is spaced from the frame. */
.statement__line--last {
  margin-top: calc(var(--statement-size) * -7.4 / 497);
  margin-bottom: calc(var(--statement-size) * -18.4 / 497);
  padding-right: calc(var(--statement-size) * 40 / 497);
  text-align: right;
}
.statement__word--last {
  font-size: calc(var(--statement-size) * 365 / 497);
  line-height: calc(359.4 / 365);
  /* Browsers add the -4% tracking after the last character too and Figma doesn't, which pushes right-aligned text 0.04em too far right. */
  padding-right: 0.04em;
}

/* The small text: Montserrat 18px on 27px, -3%, capitals (Figma's "body 18", not yet a design-system style).
   Where it fits, it sits in Figma's spot above the tops of "chola" (8px above Nichola's line), placed and sized with the big words so it
   stays clear of the h and l (18px at full size, down to 14px). It fits from 1024 wide: any narrower and Nichola is too small, so the text
   would wrap onto a fourth line and into the o. Below that it goes under "Hudson." at 18px, until those sizes are designed. */
.statement__intro {
  font-family: var(--font-body);
  font-weight: 400;
  font-size: 18px;
  line-height: 1.5;
  letter-spacing: -0.03em;
  text-transform: uppercase;
  max-width: 375px;
  margin: var(--space-32) var(--menu-inset) 0 auto;
}
@media (max-width: 767px) {
  .statement__intro { margin-right: var(--menu-inset-phone); margin-left: var(--menu-inset-phone); }
}
@media (min-width: 1024px) {
  .statement__intro {
    position: absolute;
    top: calc(var(--statement-pad) - var(--statement-size) * 8 / 497);
    left: calc(var(--statement-size) * 712 / 497);
    width: calc(var(--statement-size) * 375 / 497);
    max-width: none;
    margin: 0;
    font-size: clamp(14px, var(--statement-size) * 18 / 497, 18px);
  }
}

/* Leaves. Each leaf SVG goes at the start of the word the leaves grow from: a 1em square standing on the baseline where the word starts,
   with negative margins so it takes no room in the line. The leaves overflow it, so they scale and move with the word at any size.
   It's inline-block rather than absolutely positioned so the word stays one word for screen readers and copying, and it's at the start of
   the word rather than before the letter so it doesn't break the kerning between letters. */
.leaf-heading__leaves {
  display: inline-block;
  width: 1em;
  height: 1em;
  margin: -1em -1em 0 0;
  vertical-align: baseline;
  overflow: visible;
  pointer-events: none;
  fill: currentColor;
}
/* Hidden until Italiana has loaded, so the leaves never sit on a fallback font. */
.statement:not(.is-ready) .leaf-heading__leaves { visibility: hidden; }

/* Each leaf turns and scales about the point where it joins the letter (--leaf-base, in its own Figma coordinates). */
.leaf-heading__leaf {
  transform-box: view-box;
  transform-origin: var(--leaf-base);
}

/* Motion. Each word fades in as it rises from behind its bottom edge, then its leaves grow.
   - "Nichola" plays on page load: the word, then the stem grows up out of the N; once it's fully grown its tall leaf grows and then the
     branch opens, then (where it sits beside Nichola) the small text. About 4.35s in all.
   - "Hudson." waits until a quarter of it is on screen: the word, then the two leaves on the s opening together like a bud, then (where it
     sits under Hudson.) the small text. If both words are on screen at once, Hudson. follows a beat behind.
   - Eased out on the same curve as the floating menu button. Once grown, each leaf sways gently about its base, out of step with the others
     (the stem stays still), and the sway pauses while the statement is off screen. site.js adds the classes that start each part.
   With reduced motion none of this runs: everything is simply there. */
.statement {
  --statement-ease: cubic-bezier(0.2, 0.7, 0.2, 1);
  --statement-rise: 1050ms;
  --leaf-unfurl-duration: 1050ms;
  --statement-intro-duration: 900ms;
  /* Nichola, from page load */
  --leaf-stem-start: 1050ms;
  --leaf-stem-duration: 600ms;
  --leaf-tall-start: 1650ms; /* stem start + duration */
  --leaf-tall-duration: 900ms;
  --leaf-branch-start: 2325ms; /* 675ms after the tall leaf */
  --statement-intro-start-first: 3450ms;
  /* Hudson., from when it starts */
  --statement-last-delay: 0ms;
  --leaf-sprout-start: calc(var(--statement-last-delay) + 1050ms);
  --statement-intro-start-last: calc(var(--statement-last-delay) + 2100ms);
}
.statement.is-last-staggered { --statement-last-delay: 225ms; }

@media (prefers-reduced-motion: no-preference) {
  /* Before each entrance. Only under .js (set in the head), so the text never stays hidden without JavaScript. */
  .js .statement__line { clip-path: inset(-100vmax -100vmax 0 -100vmax); } /* clips the bottom edge only, so the leaves above aren't cut */
  .js .statement:not(.is-first-entered) .statement__word--first,
  .js .statement:not(.is-last-entered) .statement__word--last { opacity: 0; transform: translateY(110%); }
  .js .statement:not(.is-first-entered) .statement__word--first .leaf-heading__leaves,
  .js .statement:not(.is-last-entered) .statement__word--last .leaf-heading__leaves { visibility: hidden; }

  .statement.is-first-entered .statement__word--first {
    animation: rise-in var(--statement-rise) var(--statement-ease) both;
  }
  .statement.is-last-entered .statement__word--last {
    animation: rise-in var(--statement-rise) var(--statement-ease) var(--statement-last-delay) both;
  }

  .statement.is-first-entered .leaf-heading__leaf--stem {
    animation: leaf-stem-grow var(--leaf-stem-duration) var(--statement-ease) var(--leaf-stem-start) both;
  }
  /* Growth sets transform; the sway sets rotate, so the two stack: the grown leaf rocks about its base. The tall leaf lengthens like a stem. */
  .statement.is-first-entered .leaf-heading__leaf--tall {
    animation:
      leaf-stem-grow var(--leaf-tall-duration) var(--statement-ease) var(--leaf-tall-start) both,
      leaf-sway var(--leaf-sway-period) ease-in-out calc(var(--leaf-tall-start) + var(--leaf-tall-duration)) infinite;
  }
  .statement.is-first-entered .leaf-heading__leaf--branch {
    animation:
      leaf-unfurl var(--leaf-unfurl-duration) var(--statement-ease) var(--leaf-branch-start) both,
      leaf-sway var(--leaf-sway-period) ease-in-out calc(var(--leaf-branch-start) + var(--leaf-unfurl-duration)) infinite;
  }
  .statement.is-last-entered .leaf-heading__leaf--sprout {
    animation:
      leaf-unfurl var(--leaf-unfurl-duration) var(--statement-ease) var(--leaf-sprout-start) both,
      leaf-sway var(--leaf-sway-period) ease-in-out calc(var(--leaf-sprout-start) + var(--leaf-unfurl-duration)) infinite;
  }

  .statement.is-offscreen .leaf-heading__leaf { animation-play-state: paused; }
}

/* The small text comes in with the word it sits beside: Nichola where it fits beside it, otherwise Hudson. */
@media (prefers-reduced-motion: no-preference) and (min-width: 1024px) {
  .js .statement:not(.is-first-entered) .statement__intro { opacity: 0; }
  .statement.is-first-entered .statement__intro {
    animation: fade-up var(--statement-intro-duration) var(--statement-ease) var(--statement-intro-start-first) both;
  }
}
@media (prefers-reduced-motion: no-preference) and (max-width: 1023px) {
  .js .statement:not(.is-last-entered) .statement__intro { opacity: 0; }
  .statement.is-last-entered .statement__intro {
    animation: fade-up var(--statement-intro-duration) var(--statement-ease) var(--statement-intro-start-last) both;
  }
}

/* The word fades in as it rises. 110%, not 100%: Nichola's tall letters reach higher than its tight 0.81em line, so a 100% drop would
   leave their tops peeking out. */
@keyframes rise-in {
  from { opacity: 0; transform: translateY(110%); }
  to { opacity: 1; transform: translateY(0); }
}
@keyframes fade-up {
  from { opacity: 0; transform: translateY(16px); }
  to { opacity: 1; transform: translateY(0); }
}
/* The stem lengthens along its own axis (--leaf-axis), starting at a third of its width and filling out as it grows.
   Squashed to nothing along its length it could still leave a sliver, so it's hidden on the first frame only. */
@keyframes leaf-stem-grow {
  from { transform: rotate(var(--leaf-axis)) scale(0, 0.35) rotate(calc(var(--leaf-axis) * -1)); visibility: hidden; }
  to { transform: rotate(var(--leaf-axis)) scale(1, 1) rotate(calc(var(--leaf-axis) * -1)); }
}
/* Branch and sprout leaves start folded (--leaf-fold) and small, and open to their Figma angle as they grow. */
@keyframes leaf-unfurl {
  from { transform: rotate(var(--leaf-fold)) scale(0); }
  to { transform: rotate(0deg) scale(1); }
}
/* A gentle rock: over, back a little past upright, and home. Starts and ends upright so it picks up seamlessly from the growth. */
@keyframes leaf-sway {
  0%, 100% { rotate: 0deg; }
  30% { rotate: var(--leaf-sway); }
  70% { rotate: calc(var(--leaf-sway) * -0.6); }
}

/* ---------- Work (from Figma: Frame 38, node 296:909) ----------
   The WorkItem list in the page's .container (index.html), whose 40px margins (24px on phones) line it up with the header's logo up to
   1600px wide. Items sit directly on top of each other; the line under each one separates it from the next.
   Clipped sideways so the hover image (in Figma's spot, 62% across) never makes the page scroll horizontally where items are narrower
   than Figma's, which pushes it past the right edge. */
.work-section { overflow-x: clip; }
.work-list {
  --work-ease: cubic-bezier(0.2, 0.7, 0.2, 1);
  --work-rise: 900ms;
  --work-stagger: 150ms; /* site.js reads this */
  padding: 0;
  list-style: none;
}

/* Entrance. Each item fades in as it rises space-48, once a quarter of it is on screen (site.js adds .is-entered). Items that come into
   view together go one after another, --work-stagger apart (site.js sets each one's --work-enter-delay). Same curve and length as the
   site's other fade-ups. It moves the <li>, not the WorkItem, so the component's own hover movement is untouched. Only under .js, so the
   list never stays hidden without JavaScript; with reduced motion the items are simply there. */
@media (prefers-reduced-motion: no-preference) {
  .js .work-list > li:not(.is-entered) { opacity: 0; }
  /* backwards, not both: once it ends the animation lets go, leaving no transform behind. A lingering one would make each item its own
     stacking context, and a hovered item's image could then cover the title above it (WorkItem keeps every title above every image). */
  .work-list > li.is-entered {
    animation: work-rise-in var(--work-rise) var(--work-ease) var(--work-enter-delay, 0ms) backwards;
  }
}
@keyframes work-rise-in {
  from { opacity: 0; transform: translateY(var(--space-48)); }
  to { opacity: 1; transform: none; }
}

/* ---------- About (from Figma: Final > Frame 5, node 428:2239) ----------
   "About me." with the photo in the gap between the m and the e, a stem with two leaves growing from the b, and a leaf on the e's tail;
   then the paragraph and three skill lists. Figma's sections are 35px apart inside the frame, kept exact.
   At 1440 wide (Figma): the heading is 286px Italiana on a 237px line, -3% tracking. Its group is 1131 wide, centred, with 112px above
   "About" for the stem and leaves; "m    e." starts 437px in and 45px up into the line above, and the photo (260 × 347) is 688px in and
   194px down. Everything in the heading is in em, so it scales as one: it's always 1131/1440 of the page wide (up to the 1600px
   container), like the statement.
   The paragraph (640px, the design system's text-max) and the skill lists (656px, three columns) start at 617 and keep their size;
   on narrower screens they stay lined up under the end of "e." and narrow only to keep inside the page margins.
   Phones: no photo, the gap closes ("About" / "me.", still indented), the heading fits between the 24px margins, and the lists stack.
   Clipped sideways: the e's leaf SVG is a 1em square starting at the e, so on phones its empty right half reaches past the edge. */
.about {
  --about-size: calc(var(--page-width) * 286 / 1440); /* the heading's font size: it stops growing with the 1600px container */
  width: 100%;
  max-width: var(--container-max);
  margin-inline: auto;
  display: flex;
  flex-direction: column;
  gap: 35px;
  overflow-x: clip;
}
.about__intro {
  position: relative;
  width: calc(1131em / 286);
  margin-inline: auto;
  padding-top: calc(112.0004em / 286); /* room above "About" for the stem and its leaves */
  font-size: var(--about-size);
}
.about__heading {
  font-family: var(--font-display);
  font-size: 1em;
  font-weight: 400;
  white-space: nowrap;
}
/* The words on screen are hidden from screen readers (the gap is four spaces); this is what they read instead. */
.about__label {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
}
.about__words,
.about__line { display: block; }
/* Tracking goes on the words: an em letter-spacing is worked out where it's declared. */
.about__word {
  display: block;
  line-height: calc(237 / 286);
  letter-spacing: -0.03em;
}
.about__line--last {
  margin-top: calc(-44.9989em / 286);
  padding-left: calc(437em / 286);
}
.about__gap { white-space: pre; }
.about__photo {
  position: absolute;
  top: calc(194.0009em / 286);
  left: calc(688em / 286);
  width: calc(260em / 286);
  height: calc(347em / 286);
}

/* The text column's right edge sits 12.5px (at 1440) inside the heading's right edge, which is half the heading's width (565.5px)
   right of the middle of the page: 553px. It narrows only to keep var(--menu-inset) from the left edge of the page. */
.about__body {
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  gap: 35px;
  width: min(656px, 50% + var(--about-size) * 553 / 286 - var(--menu-inset));
  margin-left: auto;
  margin-right: calc(50% - var(--about-size) * 553 / 286);
}
.about__text { max-width: var(--text-max); }
.about__skills {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  align-items: start;
  column-gap: 35px;
}
.about__skill-group {
  display: flex;
  flex-direction: column;
  gap: var(--space-16);
}

@media (max-width: 767px) {
  /* "me." without the gap ends 823.264px (at Figma's size) from the start of "About", so that's the width that fits between the margins. */
  .about { --about-size: calc((100vw - 2 * var(--menu-inset-phone)) * 286 / 823.264); }
  .about__intro { width: calc(823.264em / 286); }
  .about__gap,
  .about__photo { display: none; }
  .about__body {
    width: auto;
    margin-inline: var(--menu-inset-phone);
  }
  .about__skills {
    grid-template-columns: minmax(0, 1fr);
    row-gap: var(--space-32);
  }
}

/* The leaves show only once Italiana has loaded, like the statement's. */
.about:not(.is-ready) .leaf-heading__leaves { visibility: hidden; }

/* Motion. Once a quarter of the heading is on screen (site.js adds .is-entered), "About" fades in as it rises from behind its bottom edge,
   and "me." and the photo follow a beat behind. Then the stem grows up out of the b; once it's fully grown its two leaves open like a bud,
   and halfway through that the e's leaf grows from the end of its tail. Once grown, the leaves sway (the stem stays still), pausing while
   the section is off screen. The paragraph and each skill list fade up once a quarter of them is on screen, but never before the words have
   risen (--fade-up-after, which site.js reads). Same curve and timings as the statement and the CV link. With reduced motion everything is
   simply there. */
.about {
  --about-ease: cubic-bezier(0.2, 0.7, 0.2, 1);
  --about-rise: 1050ms;
  --about-last-delay: 225ms;
  --about-stem-start: 1050ms;
  --about-stem-duration: 900ms;
  --about-b-leaves-start: 1950ms; /* stem start + duration */
  --about-b-leaves-duration: 1050ms;
  --about-e-leaf-start: 2475ms; /* halfway through the b's leaves */
  --about-e-leaf-duration: 1050ms;
  --about-text-duration: 900ms;
  --fade-up-after: 1050ms;
}
@media (prefers-reduced-motion: no-preference) {
  /* Each line clips only below itself, so the leaves above aren't cut. */
  .js .about__line { clip-path: inset(-100vmax -100vmax 0 -100vmax); }
  .js .about:not(.is-entered) .about__word { opacity: 0; transform: translateY(110%); }
  .js .about:not(.is-entered) .leaf-heading__leaves { visibility: hidden; }
  .js .about:not(.is-entered) .about__photo,
  .js .about [data-fade-up]:not(.is-entered) { opacity: 0; }

  .about.is-entered .about__word--first { animation: rise-in var(--about-rise) var(--about-ease) both; }
  .about.is-entered .about__word--last { animation: rise-in var(--about-rise) var(--about-ease) var(--about-last-delay) both; }
  .about.is-entered .about__photo { animation: fade-in var(--about-rise) var(--about-ease) var(--about-last-delay) both; }
  .about.is-entered .about__stem {
    animation: leaf-stem-grow var(--about-stem-duration) var(--about-ease) var(--about-stem-start) both;
  }
  .about.is-entered .about__leaf--b {
    animation:
      leaf-unfurl var(--about-b-leaves-duration) var(--about-ease) var(--about-b-leaves-start) both,
      leaf-sway var(--leaf-sway-period) ease-in-out calc(var(--about-b-leaves-start) + var(--about-b-leaves-duration)) infinite;
  }
  .about.is-entered .about__leaf--e {
    animation:
      leaf-unfurl var(--about-e-leaf-duration) var(--about-ease) var(--about-e-leaf-start) both,
      leaf-sway var(--leaf-sway-period) ease-in-out calc(var(--about-e-leaf-start) + var(--about-e-leaf-duration)) infinite;
  }
  .about [data-fade-up].is-entered {
    animation: fade-up var(--about-text-duration) var(--about-ease) var(--fade-up-delay, 0ms) both;
  }
  .about.is-offscreen .leaf-heading__leaf { animation-play-state: paused; }
}
@keyframes fade-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

/* ---------- CV link (from Figma: Frame 46, node 428:2554; the "cv link" component, 428:2571) ----------
   "Check out my CV" in Italiana, 77px with -2% tracking on its natural line (1.178em), centred. The component is 578.716 by 157.084:
   39px above the line for the leaf on the V and 27.084px below it for the sprig on the y. Figma's frame adds 56px above the component.
   It stays 77px wherever it fits and shrinks just enough to fit inside the page margins on narrower screens; everything inside is in em,
   so the leaves scale with it. Over it the pointer becomes the dark hover button ("View my CV"; Figma's component says "Click me"). */
.cv-section { padding-top: 56px; }
.cv-link {
  display: block;
  width: calc(578.716em / 77);
  margin-inline: auto;
  padding: calc(39em / 77) 0 calc(27.084em / 77);
  font-family: var(--font-display);
  font-size: min(77px, (100vw - 2 * var(--menu-inset)) * 77 / 578.716);
  font-weight: 400;
  line-height: normal;
  white-space: nowrap;
  text-decoration-line: none;
}
@media (max-width: 767px) {
  .cv-link { font-size: min(77px, (100vw - 2 * var(--menu-inset-phone)) * 77 / 578.716); }
}
/* The mask is the edge the text rises from behind (see Motion); the text is inline-block so it can move. */
.cv-link__mask { display: inline-block; }
.cv-link__text { display: inline-block; letter-spacing: -0.02em; }

/* Screens that can't hover never see the hover button, so they get a visible button instead: the design system's .hover-button-touch,
   which shows only where the main pointer can't hover (phones, most tablets). The standard Button, saying the same as the hover label,
   centred space-32 under the line. It's part of the link, so tapping it opens the CV like the rest of the line. */
.cv-link__touch {
  margin-top: var(--space-32);
  text-align: center;
}

/* The leaves show only once Italiana has loaded, like the statement's. */
.cv-section:not(.is-ready) .leaf-heading__leaves { visibility: hidden; }

/* Motion. Once a quarter of the line is on screen (site.js adds .is-entered), the text fades in as it rises from behind its bottom edge,
   then the leaf on the V lengthens out of the stroke, then the sprig on the y opens like a bud: the same timings and curve as the statement's
   Nichola. Once grown, the leaves sway, pausing while the section is off screen. With reduced motion everything is simply there. */
.cv-section {
  --cv-ease: cubic-bezier(0.2, 0.7, 0.2, 1);
  --cv-rise: 1050ms;
  --cv-leaf-v-start: 1050ms;
  --cv-leaf-v-duration: 900ms;
  --cv-sprig-start: 1725ms;
  --cv-sprig-duration: 1050ms;
}
@media (prefers-reduced-motion: no-preference) {
  /* The mask clips only below the line (0.05em lower, so the very end of the y's tail isn't trimmed). The leaves sit outside it. */
  .js .cv-link__mask { clip-path: inset(-100vmax -100vmax -0.05em -100vmax); }
  .js .cv-section:not(.is-entered) .cv-link__text { opacity: 0; transform: translateY(110%); }
  .js .cv-section:not(.is-entered) .leaf-heading__leaves { visibility: hidden; }
  .js .cv-section:not(.is-entered) .cv-link__touch { opacity: 0; }

  .cv-section.is-entered .cv-link__text { animation: rise-in var(--cv-rise) var(--cv-ease) both; }
  /* The touch button fades up once the line has risen, while the leaves grow. */
  .cv-section.is-entered .cv-link__touch { animation: fade-up 900ms var(--cv-ease) var(--cv-rise) both; }
  .cv-section.is-entered .cv-link__leaf--v {
    animation:
      leaf-stem-grow var(--cv-leaf-v-duration) var(--cv-ease) var(--cv-leaf-v-start) both,
      leaf-sway var(--leaf-sway-period) ease-in-out calc(var(--cv-leaf-v-start) + var(--cv-leaf-v-duration)) infinite;
  }
  .cv-section.is-entered .cv-link__leaf--sprig {
    animation:
      leaf-unfurl var(--cv-sprig-duration) var(--cv-ease) var(--cv-sprig-start) both,
      leaf-sway var(--leaf-sway-period) ease-in-out calc(var(--cv-sprig-start) + var(--cv-sprig-duration)) infinite;
  }
  .cv-section.is-offscreen .leaf-heading__leaf { animation-play-state: paused; }
}

/* ---------- Contact (from Figma: Frame 31, node 256:221) ----------
   The page's one inverted band, flush with the bottom of the page: two rows of "Contact me ." drifting in opposite
   directions. The whole band is a button that opens the contact form; over it the pointer becomes the light hover button ("Get in touch").
   Desktop sizes are Figma's (187px Italiana, -0.02em tracking, a 493px band). Everything inside the band is in em,
   so it scales with the text: 136px on tablets and 96px on phones. */
.contact-section {
  margin-top: var(--section-gap);
  padding-top: var(--space-48);
}
@media (min-width: 768px) {
  .contact-section { padding-top: var(--space-64); }
}
@media (min-width: 1024px) {
  .contact-section { padding-top: var(--space-96); }
}

.contact-band {
  --contact-loop: 20s; /* how long one "Contact me ." takes to pass, at every screen size */
  display: flex;
  flex-direction: column;
  gap: calc(5em / 187);
  box-sizing: border-box;
  width: 100%;
  margin: 0;
  padding: calc(24em / 187) 0;
  border: 0;
  border-radius: var(--radius-none);
  overflow: hidden;
  text-align: left;
  cursor: pointer;
  font-family: var(--font-display);
  font-size: 96px;
  font-weight: 400;
  line-height: calc(220em / 187);
  letter-spacing: -0.02em;
  white-space: nowrap;
  text-decoration-line: none;
}
@media (min-width: 768px) {
  .contact-band { font-size: 136px; }
}
@media (min-width: 1024px) {
  .contact-band { font-size: 187px; }
}
/* The band's focus outline goes inside it, where white shows against black. */
.contact-band:focus-visible { outline-offset: calc(-1 * (var(--focus-width) + var(--focus-offset))); }

.contact-band__track {
  display: flex;
  width: max-content;
  will-change: transform;
  animation-duration: var(--contact-loop);
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}
.contact-band__unit {
  display: flex;
  gap: calc(52em / 187);
  padding-right: calc(52em / 187);
}
/* The second row starts 337px further left, as in Figma. */
.contact-band__row--right .contact-band__track { animation-name: contact-drift-right; }
.contact-band__row--left .contact-band__track { animation-name: contact-drift-left; transform: translateX(calc(-337em / 187)); }

/* Each track holds six identical units, so moving it a sixth of its width moves exactly one unit and the loop is seamless. */
@keyframes contact-drift-right {
  from { transform: translateX(calc(-100% / 6)); }
  to { transform: translateX(0); }
}
@keyframes contact-drift-left {
  from { transform: translateX(calc(-337em / 187)); }
  to { transform: translateX(calc(-100% / 6 - 337em / 187)); }
}

/* With reduced motion on, the rows stand still in Figma's positions. */
@media (prefers-reduced-motion: reduce) {
  .contact-band__track { animation: none; }
}

/* ---------- Contact form popup ----------
   Opened by every Contact me: the band, the header's button and the menu's link (site.js). A white panel over the darkened page;
   on phones it fills the screen. No Figma design yet, so it's built from the design system's rules: a visible label above each
   field, surface-muted fields 48px tall with no placeholder text, square corners, no borders or shadows, and the standard Button.
   It fades in with the menu's timings (300ms) while the panel rises space-24 (450ms); closing plays it back faster. */

/* The menu's Contact me is a button (it opens the popup rather than going anywhere), so it drops the browser's button styles
   and keeps the NavLink look. The NavLink still owns padding-left, which is how it slides. */
button.nav-link {
  margin: 0;
  padding-block: 0;
  padding-right: 0;
  border: 0;
  background: none;
  font: inherit;
  text-align: left;
  cursor: pointer;
}

/* The page behind doesn't scroll while the popup is open. */
html.contact-is-open { overflow: hidden; }

.contact-dialog {
  box-sizing: border-box;
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  max-width: none;
  max-height: none;
  margin: 0;
  padding: var(--space-48);
  border: 0;
  /* The darkened page: black, fixed in both themes like the menu's background. */
  background: color-mix(in srgb, var(--black) 80%, transparent);
  color: var(--ink);
  opacity: 0;
  transition: opacity 250ms ease-in;
}
.contact-dialog[open] {
  display: flex;
  align-items: center;
  justify-content: center;
}
.contact-dialog::backdrop { background: transparent; }
.contact-dialog.is-open { opacity: 1; transition: opacity 300ms ease-out; }

.contact-dialog__panel {
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  gap: var(--space-32);
  width: 100%;
  max-width: calc(var(--text-max) + 2 * var(--space-48));
  max-height: 100%;
  overflow-y: auto;
  padding: var(--space-48);
  background: var(--surface);
  transform: translateY(var(--space-24));
  transition: transform 250ms ease-in;
}
.contact-dialog.is-open .contact-dialog__panel {
  transform: none;
  transition: transform 450ms cubic-bezier(0.2, 0.7, 0.2, 1);
}

.contact-dialog__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-24);
}
/* The X: the menu's, in ink, with the same 48px tap area. */
.contact-dialog__close {
  display: grid;
  flex: none;
  place-items: center;
  width: var(--menu-close-size);
  height: var(--menu-close-size);
  margin: 0;
  padding: 0;
  border: 0;
  border-radius: var(--radius-none);
  background: none;
  color: inherit;
  cursor: pointer;
}

.contact-form,
.contact-form__fields,
.contact-form__field {
  display: flex;
  flex-direction: column;
}
.contact-form { gap: var(--space-32); }
.contact-form__fields { gap: var(--space-24); }
.contact-form__field { gap: var(--space-8); }
.contact-form[hidden] { display: none; }

.contact-form__input {
  box-sizing: border-box;
  width: 100%;
  height: var(--space-48);
  margin: 0;
  padding: 0 var(--space-16);
  border: 0;
  border-radius: var(--radius-none);
  background: var(--surface-muted);
  color: var(--ink);
  appearance: none;
}
/* The message box starts six lines tall, with its first line where a single-line field's text sits. */
.contact-form__input--message {
  height: auto;
  min-height: 160px;
  padding-block: calc((var(--space-48) - 26px) / 2);
  resize: vertical;
}
/* No accent colour for errors: the message says what's wrong, in ink, under the field. */
.contact-form__error { color: var(--ink); font-weight: 500; }

.contact-form__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-16) var(--space-24);
}
.contact-form__status:empty { display: none; }
/* The privacy line, 16px under the Send button (the form's gap is 32px). */
.contact-form__privacy { margin: calc(var(--space-16) - var(--space-32)) 0 0; }

/* ---------- Thank-you screen (from Figma: Frame 54, node 688:6907) ----------
   Once a message has gone, a full white screen fades in over the popup with "Thank you" and the line under it, centred. Then "Thank you"
   fades up, the two leaves on its u open like the s's on "Hudson." (same leaves, same timings), and "I'll be in touch soon" fades up.
   Figma's sizes at 92px: Italiana at -0.03em on its natural line, and body-lower 11px below it. The title shrinks on narrow screens so the
   leaves stay inside the phone margin, and the leaves, drawn in em, shrink with it. The X sits where the menu's does; a click anywhere closes the popup. */
.contact-thanks {
  --thanks-ease: cubic-bezier(0.2, 0.7, 0.2, 1);
  position: fixed;
  inset: 0;
  display: grid;
  place-items: center;
  padding: var(--space-24);
  background: var(--surface);
  color: var(--ink);
  cursor: pointer;
}
.contact-thanks[hidden] { display: none; }
.contact-thanks__message {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 11px;
  text-align: center;
}
.contact-thanks__title {
  position: relative;
  font-family: var(--font-display);
  /* The word is centred and the leaves hang 21.5px (at 92px) past its end, so the right half is 188 + 21.5 Figma px wide:
     shrink until that fits in half the screen, less the phone margin. */
  font-size: min(92px, (50vw - var(--margin-phone)) * 92 / 209.5);
  font-weight: 400;
  line-height: normal;
  letter-spacing: -0.03em;
  white-space: nowrap;
}
.contact-thanks__title:focus { outline: none; } /* it takes focus only so screen readers read it */
.contact-thanks__leaves {
  position: absolute;
  top: 0;
  left: 0;
  width: calc(376em / 92);
  height: calc(108em / 92);
  overflow: visible;
  fill: currentColor;
  pointer-events: none;
}
.contact-thanks__close {
  position: absolute;
  top: var(--menu-close-top);
  right: calc(var(--menu-inset) - var(--menu-close-size) / 2);
  display: grid;
  place-items: center;
  width: var(--menu-close-size);
  height: var(--menu-close-size);
  padding: 0;
  border: 0;
  border-radius: var(--radius-none);
  background: none;
  color: inherit;
  cursor: pointer;
}
@media (max-width: 767px) {
  .contact-thanks { --menu-inset: var(--menu-inset-phone); }
}

@media (prefers-reduced-motion: no-preference) {
  .contact-thanks.is-shown { animation: fade-in 400ms ease-out both; }
  .contact-thanks.is-shown .contact-thanks__title { animation: fade-up 1050ms var(--thanks-ease) 300ms both; }
  .contact-thanks.is-shown .contact-thanks__leaf {
    animation:
      leaf-unfurl 1050ms var(--thanks-ease) 1350ms both,
      leaf-sway var(--leaf-sway-period) ease-in-out 2400ms infinite;
  }
  .contact-thanks.is-shown .contact-thanks__line { animation: fade-up 900ms var(--thanks-ease) 2400ms both; }
}

/* Phones: the panel fills the screen. */
@media (max-width: 767px) {
  .contact-dialog { padding: 0; }
  .contact-dialog__panel {
    max-width: none;
    height: 100%;
    padding: var(--space-24);
  }
}

@media (prefers-reduced-motion: reduce) {
  .contact-dialog,
  .contact-dialog__panel { transition: none !important; }
  .contact-dialog__panel { transform: none; }
}

/* ---------- Case study pages (from Figma: Final > Desktop - 11, node 468:5377, the Lodders page) ----------
   Figma's Frame 39: one centred column of blocks, 72px apart. First the "case study top" component (title, intro and four details, 1128px
   wide), then the project image, then the text (Frame 42: a 666px column of sections, each an Italiana heading over body-lower text, 32px
   apart). Further images and text blocks follow in the same column, 72px apart (RISE: Frame 52). Each block keeps its Figma width where
   it fits and otherwise fills the room inside the page margins (menu-inset, like the header).
   The page's bottom padding is the gap above the Contact band (128px at 1440, as in Figma; the site's section gap on smaller screens).
   Figma's type styles that the design system doesn't have yet live here: "case study title" (Italiana 106, -2%; 80px on tablets, 48px on
   phones), "case study headings" (Italiana 39, -1%), "body key points" (Montserrat 22/27, -3%) and "body 18" (Montserrat 18/27, -3%, in
   capitals). Italiana's "normal" line height is 1.178em. The RISE and SCoR pages (Desktop - 12, 13) use the same blocks. */
/* The page's .container (on <main>) gives the side margins (40px, 24px on phones) and the 1600px cap. */
.case-study {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 72px;
  padding-bottom: var(--section-gap);
}
/* The Contact band follows straight on: the gap above it is the page's bottom padding, not the home page's spacing. */
.case-study + .contact-section { margin-top: 0; padding-top: 0; }

.case-study-top {
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  gap: var(--space-48);
  width: min(1128px, 100%);
  padding-block: 40px;
}
.case-study-top__title {
  font-family: var(--font-display);
  font-size: 106px;
  font-weight: 400;
  line-height: normal;
  letter-spacing: -0.02em;
}
@media (max-width: 1023px) {
  .case-study-top__title { font-size: 80px; }
}
@media (max-width: 767px) {
  .case-study-top__title { font-size: 48px; }
}
.case-study-top__intro {
  font-family: var(--font-body);
  font-size: 22px;
  font-weight: 400;
  line-height: 27px;
  letter-spacing: -0.03em;
}

/* The details (Figma: "case study info"): a capitals label over its value, 4px apart, in a row 48px apart that wraps 24px apart where
   it runs out of room. On phones, a 2 × 2 grid. */
.case-study-info {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-24) var(--space-48);
  margin: 0;
}
.case-study-info__item {
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.case-study-info__label {
  font-family: var(--font-body);
  font-size: 18px;
  font-weight: 400;
  line-height: 27px;
  letter-spacing: -0.03em;
  text-transform: uppercase;
}
.case-study-info__value { margin: 0; }
/* Browsers measure Montserrat a hair wider than Figma, so a row that just fits in Figma (SCoR's: 1128 of 1128px) would wrap. The row may
   run up to space-8 past the top block's right edge before it wraps. */
@media (min-width: 768px) {
  .case-study-info { margin-right: calc(-1 * var(--space-8)); }
}
@media (max-width: 767px) {
  .case-study-info {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-24);
  }
}

/* Project images (Figma: Group 4 on Lodders, Groups 8 and 9 on RISE), each exported from Figma as one image with the drop shadows, which
   reach past the screenshots. The box is the size of the Figma group (--box-w × --box-h), so the layout matches Figma, and the image
   hangs out of it by the shadows: --img-x to the left and --img-y above, --img-w wide. All four are set on the element, in Figma px
   without units. It shrinks with the page on narrow screens; the shadows stay inside the page margins. */
.case-study-image {
  position: relative;
  width: min(calc(var(--box-w) * 1px), 100%);
  aspect-ratio: var(--box-w) / var(--box-h);
}
.case-study-image img {
  position: absolute;
  top: calc(var(--img-y) / var(--box-h) * -100%);
  left: calc(var(--img-x) / var(--box-w) * -100%);
  width: calc(var(--img-w) / var(--box-w) * 100%);
  height: auto;
}
/* A wide image (RISE's strips, Group 9). Centred and shrinking to fit, like the others, except on phones: there it stays at least half
   Figma's size, starts at the left margin, and is cut off at the right edge of the screen. The wrapper reaches both edges of the screen
   and clips sideways only, so the shadows above and below still show, the left shadow sits in the margin, and the page never scrolls
   sideways. */
.case-study-wide {
  align-self: stretch;
  display: flex;
  justify-content: center;
}
@media (max-width: 767px) {
  .case-study-wide {
    justify-content: flex-start;
    margin-inline: calc(-1 * var(--menu-inset-phone));
    padding-left: var(--menu-inset-phone);
    overflow-x: clip;
  }
  .case-study-wide > .case-study-image {
    flex: none;
    width: max(calc(var(--box-w) * 0.5px), 100% - var(--menu-inset-phone));
  }
}

.case-study-text {
  display: flex;
  flex-direction: column;
  gap: var(--space-32);
  width: min(666px, 100%);
}
.case-study-section {
  display: flex;
  flex-direction: column;
  gap: var(--space-16);
}
.case-study-section__heading {
  font-family: var(--font-display);
  font-size: 39px;
  font-weight: 400;
  line-height: normal;
  letter-spacing: -0.01em;
}
/* Paragraphs that follow on from each other are a blank line apart, as in Figma (30px: one line of body-lower). */
.case-study-prose {
  display: flex;
  flex-direction: column;
  gap: 30px;
}
/* Figma indents the results list 16px. */
.case-study-list { padding-left: var(--space-16); }
/* ---------- The Lodders line (Figma: Group 2, 468:5398; the SVG from the Lodders case study doc) ----------
   The Lodders brand pattern with its white line, written into the page so the line can draw. 500px wide, centred in the text column
   (your call; Figma's spot is 329 × 329 on the left), shrinking to the page margins on phones. Once it has faded up ([data-fade-up]),
   the white line draws itself from the right edge through the maze to the left edge, then stays: 3.5s with the SCoR drawing's
   in-and-out easing, starting when the fade-up ends (--fade-up-delay plus --case-study-fade). The path has pathLength="1" and runs left
   to right, so one dash as long as the path, slid in from beyond its end (stroke-dashoffset -1 to 0), draws it from the right. Before
   then (motion allowed, JavaScript running) the line is hidden; without JavaScript or with reduced motion it's simply there.
   48px above and below, like the SCoR drawing (your call), not the section's 16px. */
.lodders-line {
  display: block;
  width: min(500px, 100%);
  height: auto;
  margin-inline: auto;
  aspect-ratio: 1396.12 / 1393.95;
}
.case-study-section > .lodders-line { margin-block: calc(var(--space-48) - var(--space-16)); }
.lodders-line__path { stroke-dasharray: 1 1; }
@media (prefers-reduced-motion: no-preference) {
  .js .lodders-line:not(.is-entered) .lodders-line__path { stroke-dashoffset: -1; }
  .lodders-line.is-entered .lodders-line__path {
    animation: line-draw-from-end 3500ms cubic-bezier(0.65, 0, 0.35, 1) calc(var(--fade-up-delay, 0ms) + var(--case-study-fade)) both;
  }
}
@keyframes line-draw-from-end {
  from { stroke-dashoffset: -1; }
  to { stroke-dashoffset: 0; }
}

/* ---------- Drawing (Figma: Final > Frame 56, 705:7179, "drawing frame 1" to "9"; on the SCoR page) ----------
   Circles traced over a photo of an MRI scanner. Built and tuned in prototypes/drawing-animation.html, whose notes explain the frames.
   Figma's frame is 600 × 400; here it fills the text column (your call), so 666 × 444 at full size, and the photo and lines scale with it
   (lines 4.4px there, about 2.2px at 375 wide). The photo fills the box and the SVG lies over it with the frame's coordinates as its
   viewBox. Behind the photo is the page background, frame 9's fill (Figma's "white"), so once the photo has faded the shape sits on the
   page as in Figma. In the case study text it has 48px above and below (your call), not the section's 16px.
   With reduced motion it shows frame 8 still: the photo with the finished shape. Otherwise it starts on frame 1, the photo alone (also
   without JavaScript), so the finished shape never shows for a moment before site.js plays it. It fades up like the other blocks
   ([data-fade-up]), and site.js starts it when that ends. */
.drawing {
  position: relative;
  width: 100%;
  aspect-ratio: 600 / 400;
  background: var(--surface);
}
.case-study-section > .drawing { margin-block: calc(var(--space-48) - var(--space-16)); }
.drawing__photo,
.drawing__lines {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
}
.drawing__lines {
  fill: none;
  stroke: var(--ink);
  stroke-width: 4;
}
/* Each path has pathLength="1", so these are fractions of the path. The ring's path starts at 3 o'clock and runs clockwise, so its
   first half is the bottom (3, 6, 9 o'clock) and its second half the top. */
.drawing__ring-bottom { stroke-dasharray: 0.5 0.5; }
.drawing__ring-top { stroke-dasharray: 0.5 0.5; stroke-dashoffset: 0.5; }
/* The top half and the line turn about their own centres (the ring's is the circles' centre, the line's is its middle). */
.drawing__ring-top,
.drawing__line {
  transform-box: fill-box;
  transform-origin: center;
}
/* Frame 8, the still for reduced motion: the half ring turned to the bottom, no line. */
.drawing__ring,
.drawing__ring-bottom,
.drawing__line { opacity: 0; }
.drawing__ring-top { transform: rotate(180deg); }
/* While it plays, the paths that draw in are one dash as long as the path, which stroke-dashoffset slides into view. */
.drawing.is-playing :is(.drawing__big, .drawing__ring, .drawing__small, .drawing__line) { stroke-dasharray: 1 1; }
/* With motion, the shape is hidden (frame 1) until the animation shows it. */
@media (prefers-reduced-motion: no-preference) {
  .drawing__shape { opacity: 0; }
}

/* Motion. Once the header has landed and Italiana is in (site.js adds .is-entered), the title rises line by line from behind its bottom
   edge, --case-study-stagger apart, like "Nichola" on the home page. Then the intro and each detail fade up 16px, one after another,
   starting --case-study-text-after behind the title's last line (site.js works out --case-study-text-start from the number of lines).
   Further down, each [data-fade-up] block fades up once a quarter of it is on screen, never before the details have started; blocks
   that arrive together go one after another. site.js wraps the title's words in .case-study-top__word spans and numbers their lines
   (--rise-line). Same curve and lengths as the rest of the site. With reduced motion everything is simply there. */
.case-study {
  --case-study-ease: cubic-bezier(0.2, 0.7, 0.2, 1);
  --case-study-rise: 1050ms;
  --case-study-fade: 900ms;
  --case-study-stagger: 150ms; /* site.js reads this */
  --case-study-text-after: 600ms; /* site.js reads this */
}
@media (prefers-reduced-motion: no-preference) {
  /* Hidden until site.js has wrapped the words, so the title never shows before its entrance. Only under .js. */
  .js .case-study-top__title:not(.is-split) { visibility: hidden; }
  /* Each word clips only below itself (its box runs to the bottom of Italiana's descenders), so the line rises from behind its bottom edge. */
  .case-study-top__word { display: inline-block; clip-path: inset(-100vmax -100vmax 0 -100vmax); }
  .case-study-top__word-inner { display: inline-block; }
  .js .case-study-top:not(.is-entered) .case-study-top__word-inner { opacity: 0; transform: translateY(110%); }
  .case-study-top.is-entered .case-study-top__word-inner {
    animation: rise-in var(--case-study-rise) var(--case-study-ease) calc(var(--rise-line, 0) * var(--case-study-stagger)) both;
  }

  .js .case-study-top:not(.is-entered) [data-top-fade] { opacity: 0; }
  .case-study-top.is-entered [data-top-fade] {
    animation: fade-up var(--case-study-fade) var(--case-study-ease)
      calc(var(--case-study-text-start, 0ms) + var(--fade-order, 0) * var(--case-study-stagger)) backwards;
  }

  /* backwards, not both: once it ends the animation lets go, leaving no transform behind. */
  .js .case-study [data-fade-up]:not(.is-entered) { opacity: 0; }
  .case-study [data-fade-up].is-entered {
    animation: fade-up var(--case-study-fade) var(--case-study-ease) var(--fade-up-delay, 0ms) backwards;
  }
}
