/* Signal — the site identity.
   Type, palette, the nav, the ink-inversion row, and the bar section on the
   home page. Loads AFTER styles.css and overrides it; styles.css still owns
   the layout. Generated by publish-identity.mjs from explorations/parked/ —
   edit there and re-run. */

:root {
  --serif: 'Manrope', Arial, sans-serif;
  --sans: 'Manrope', Arial, sans-serif;
  --mono: 'Manrope', Arial, sans-serif;
  /* The whole neutral ramp, not just the top of it. styles.css tints its two
     shades toward a cream page; leaving them behind meant 47 surfaces — the
     namespace dropdown loudest among them — went on painting beige on white.
     Each step keeps the paper's own faint blue cast and matches the luminance
     drop the old ramp had (6.5 and 6.3), so hovers stay as quiet as they were. */
  --paper: #f9f9fc;
  --paper-warm: #f1f1f4;
  --paper-deep: #eaeaec;
  --ink-faintest: #c2c2c5;      /* the luminance the original #C9C2B2 had */
  --ink-faint: #959597;         /* likewise #9A958A, whose warmth showed on small labels */

  --accent: #c93123;
  --accent-deep: #ac271c;
  /* A tint of this page's red, not the old salmon — that was mixed toward
     cream and goes muddy on white. Lighter, too: the current row already says
     so with a tick and red text, and the fill should be the quietest of the
     three signals rather than the loudest. */
  --accent-soft: #f2ddde;

  /* One curve for everything that inverts. Slow out of the gate, quick through
     the middle, long settle — it reads as the row leaning in rather than
     snapping, which is what makes the pull-inward feel physical. */
  --signal-curve: cubic-bezier(.22, .68, .16, 1);
  --signal-in: 460ms;
  --signal-out: 620ms;
}

/* ---------- display type ---------- */
.hero.v5 .h-block h1.hx-display,
.section-title,
.work-list .item .title,
.contact-block .l h2,
.snip-pop__name,
.principle h2,
.shelf-title,
.notes-year .year,
.page-hero-grid .l h1,
.library-h1,
/* The bio lead is two voices now: the name in the title face, what he does in
   the sans below it. So the display rule takes the name rather than the whole
   paragraph. */
.about-page-grid .bio .lead-name,
/* The case studies and the about page were outside the identity until the
   publisher learned to find them; their headings belong to the same voice.
   The case hero is reached at .case-hero.case-hero--wide .case-title in
   styles.css, so a bare class here loses to it — matched rather than assumed. */
.case-hero.case-hero--wide .case-title,
.case-grid .body-block h2,
.case-prose h2,
.takeaways__head {
  /* Fraunces 900 is the title face. The --display-* hooks stay so another can
     still be tried from devtools without editing this file; the nav picker that
     used to drive them is gone. Candidates still live in display-fonts.css and
     title-fonts.html sets them all side by side. */
  font-family: var(--display-font, 'Fraunces'), Georgia, serif;
  font-weight: var(--display-weight, 900);
  letter-spacing: var(--display-track, -0.025em);
  /* Holds the cap height at Archivo Black's ratio, so changing the face did not
     quietly resize every heading across four pages. */
  font-size-adjust: cap-height 0.73;
  /* No synthesised bold or oblique — a family missing the cut being asked for
     should show its real one, not a smeared approximation. */
  font-synthesis: none;
}

.hero.v5 .h-block h1 .b,
.section-title em,
.work-list .item .title em,
.contact-block .l h2 em,
.principle h2 em,
.notes-year .year em,
.page-hero-grid .l h1 em,
.library-h1 em,
.case-hero.case-hero--wide .case-title em,
.case-grid .body-block h2 em,
.case-prose h2 em,
.about-page-grid .bio .lead-name em {
  font-style: normal;
}

/* ---------- the bio lead ----------
   His name, then what he does — set as two things rather than one sentence in
   one face. The paragraph itself stops carrying the title styling; each half
   asks for its own. */
.about-page-grid .bio .lead-name {
  display: block;
  font-size: clamp(26px, 2.9vw, 38px);
  line-height: 1.1;
}
.about-page-grid .bio .lead-role {
  display: block;
  font-family: var(--sans);
  font-weight: 400;
  /* Roughly half the name's size — far enough apart that the two read as a
     hierarchy rather than as one setting that changed its mind. */
  font-size: clamp(15px, 1.45vw, 18px);
  line-height: 1.5;
  letter-spacing: 0;
  color: var(--ink-2);
  margin-top: 12px;
  /* Its own measure: 38ch of the paragraph's size is a long way at this one. */
  max-width: 30ch;
}

.hero.v5 .dispatches h4,
.disciplines .d h4,
.clients-strip h3,
.snip .info h4,
.process .step h4,
.principle .disc,
.note-entry .title {
  font-family: var(--sans);
  font-weight: 600;
}

.hero.v5 .dispatches h4 em,
.clients-strip h3 em,
.process .step .top .big,
.note-entry .title em {
  font-style: normal;
}

/* ---------- nav ----------
   Solid paper, not a translucent blur. The blur let the red bar read straight
   through the header, which made anything landing slightly under it look like
   a rendering fault rather than a scrolled page. */
nav.top {
  background: var(--paper);
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}

/* Jumping to a section has to clear that nav with room to spare. The lazy
   hats image sits directly above #clients with no intrinsic dimensions, so it
   measures zero until its bytes arrive and then pops — which is what was
   throwing the Work anchor: the scroll was computed against a document that
   grew by the height of an illustration a moment later. Reserving its box from
   the aspect ratio makes the landing deterministic. */
.section { scroll-margin-top: 88px; }
.about-grid .hats-art img,
.hats-art--inline img { aspect-ratio: 1894 / 830; }

/* The name, left, in the title face — a wordmark rather than a file path, set
   lowercase the way the reference draws it. Fraunces at 900 has enough colour
   in the lowercase that it holds its own against the nav opposite without
   needing caps to do the work. The two declarations after the family are both
   defensive: styles.css uppercases this element and adds mono tracking to it,
   and neither belongs on a serif wordmark. */
.brand-switch .ns {
  font-family: 'Fraunces', Georgia, serif;
  font-weight: 900;
  font-size: 21px;
  letter-spacing: -0.012em;
  color: var(--ink);
  text-transform: lowercase;
  font-variant-caps: normal;
}
.brand-switch:hover .ns { color: var(--accent); }
/* The namespace the trigger stands for stays in the accessibility tree: a
   switcher that does not say what it is switching from reads as a bare name. */
.ns-sr {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
/* The chevron is the whole signal that this is a menu now, so it keeps its
   place and picks up the accent with the name. */
.brand-switch .chev {
  margin-left: 7px;
  color: var(--ink-faint);
  /* The triangle's ink sits high in its own em box, so centring the box leaves
     it riding above the small caps. Drop it to the optical middle. */
  transform: translateY(2px);
  transition: color var(--m-fast);
}
.brand-switch:hover .chev { color: var(--accent); }
.brand-switch[aria-expanded='true'] .chev { color: var(--accent); }

/* Sans, Title Case, sitting right — and the page you are on drops out of that
   rhythm entirely into Fraunces italic in red. The point of the Ogilvy nav is
   that the current item is not the same word emphasised; it is a different
   voice. Weight or colour alone would read as a hover state. */
nav.top ul.primary {
  font-family: var(--sans);
  font-size: 15px;
  font-weight: 500;
  text-transform: capitalize;
  letter-spacing: 0.005em;
  gap: clamp(18px, 2.1vw, 32px);
  align-items: baseline;
}
nav.top ul.primary a {
  color: var(--ink-2);
  transition: color var(--m-fast);
}
/* Ink to a darker ink was a hover you had to look for. Red is the one colour
   the page already spends on the thing you are pointing at. */
nav.top ul.primary a:hover { color: var(--accent); }
nav.top ul.primary a[aria-current='page'] {
  font-family: 'Fraunces', Georgia, serif;
  font-weight: 900;
  font-style: italic;
  font-size: 18px;
  letter-spacing: -0.01em;
  color: var(--accent);
}
nav.top ul.primary a[aria-current='page']:hover { color: var(--accent-deep); }

/* ---------- the ink row ----------
   The identity's one interaction. A row inverts to ink on hover and, at the
   same time, draws its own contents inward — the band stays put at full width
   while the text and the arrow walk in from both edges. Reaching for a row
   narrows it, which makes the pointer feel like it has weight.

   `--row-pad` is the whole mechanism: padding, not transform, so the ink band
   is unaffected and only the content moves. Both directions are transitioned on
   the base rule, so leaving animates out on its own, a little slower than it
   came in. */
/* ---------- 04 selected work ---------- */
.work-list .item {
  --row-pad: 0px;
  padding-left: var(--row-pad);
  padding-right: var(--row-pad);
  background-color: transparent;
  transition:
    background-color var(--signal-out) var(--signal-curve),
    padding var(--signal-out) var(--signal-curve);
}
.work-list .item:hover {
  --row-pad: clamp(18px, 2.8vw, 48px);
  background-color: var(--ink);
  transition-duration: var(--signal-in);
}
/* Every child fades independently on the same curve, so the type lightens as
   the ground darkens instead of flashing at the end of the background fade. */
.work-list .item .title,
.work-list .item .title em,
.work-list .item .desc,
.work-list .item .meta-tags,
.work-list .item .arrow {
  transition: color var(--signal-out) var(--signal-curve);
}
.work-list .item:hover .title { color: var(--paper); }
/* The project name keeps its accent on the dark row — it is the reason the em
   is there, and it gives the row a second red note beside the arrow. */
.work-list .item:hover .title em { color: var(--accent); }
.work-list .item:hover .desc { color: #a8a29a; }
.work-list .item:hover .meta-tags { color: #8d877e; }
.work-list .item:hover .arrow { color: var(--accent); }
.work-list .item:hover,
.work-list .item:hover .title,
.work-list .item:hover .desc,
.work-list .item:hover .meta-tags,
.work-list .item:hover .arrow {
  transition-duration: var(--signal-in);
}
/* The arrow already had a nudge; it now rides on top of the inward pull. */
.work-list .item .arrow {
  transition: color var(--signal-out) var(--signal-curve),
              transform var(--signal-in) var(--signal-curve);
}

/* ---------- 06 process CTA ----------
   Same inversion, no inward pull — the band is a destination, not a list row,
   and pulling its single line inward would read as a wobble. */
.process-more {
  background: var(--paper);
  border-top-color: var(--accent);
  border-left-color: var(--accent);
  transition:
    background-color var(--signal-out) var(--signal-curve),
    border-color var(--signal-out) var(--signal-curve);
}
.process-more .lead,
.process-more .lead em,
.process-more .arrow {
  transition: color var(--signal-out) var(--signal-curve),
              transform var(--signal-in) var(--signal-curve);
}
.process-more:hover {
  background-color: var(--ink);
  border-top-color: var(--accent);
  border-left-color: var(--accent);
  transition-duration: var(--signal-in);
}
.process-more:hover .lead { color: var(--paper); }
.process-more:hover .lead em { color: var(--accent); }
.process-more:hover .arrow { color: var(--accent); }
.process-more:hover .lead,
.process-more:hover .lead em,
.process-more:hover .arrow { transition-duration: var(--signal-in); }

/* ---------- process page ----------
   Colour inversion only — no inward pull. The cards are in a hairline grid, so
   they start on paper rather than transparent: going transparent would expose
   the grid's own black gutter behind every card at once. And unlike a list row,
   a card holds a paragraph — moving its padding reflows five lines of body copy
   mid-transition, which reads as a shudder rather than a grab. */
.principle {
  background-color: var(--paper);
  transition: background-color var(--signal-out) var(--signal-curve);
}
.principle:hover {
  background-color: var(--ink);
  transition-duration: var(--signal-in);
}
.principle h2,
.principle h2 em,
.principle p,
.principle .aside,
.principle .disc {
  transition: color var(--signal-out) var(--signal-curve);
}
.principle:hover h2 { color: var(--paper); }
.principle:hover h2 em { color: var(--accent); }
.principle:hover p { color: #a8a29a; }
/* The pull-quote stays light rather than red — it is small italic text, and red
   on ink only clears 3.34:1, which is a large-text number. */
.principle:hover .aside { color: #d8d3ca; }
.principle:hover .disc { color: #8d877e; }
.principle:hover h2,
.principle:hover h2 em,
.principle:hover p,
.principle:hover .aside,
.principle:hover .disc { transition-duration: var(--signal-in); }

/* ---------- notes ---------- */
.note-entry {
  --row-pad: 0px;
  padding-left: var(--row-pad);
  padding-right: var(--row-pad);
  background-color: transparent;
  transition:
    background-color var(--signal-out) var(--signal-curve),
    padding var(--signal-out) var(--signal-curve);
}
.note-entry:hover,
.note-entry:focus-visible {
  --row-pad: clamp(18px, 2.8vw, 48px);
  background-color: var(--ink);
  transition-duration: var(--signal-in);
}
.note-entry .date,
.note-entry .title,
.note-entry .title em,
.note-entry .where,
.note-entry .arrow {
  transition: color var(--signal-out) var(--signal-curve),
              transform var(--signal-in) var(--signal-curve);
}
.note-entry:hover .title { color: var(--paper); }
.note-entry:hover .title em { color: var(--accent); }
.note-entry:hover .date { color: #8d877e; }
.note-entry:hover .where { color: #8d877e; }
.note-entry:hover .arrow { color: var(--accent); }
.note-entry:hover .date,
.note-entry:hover .title,
.note-entry:hover .title em,
.note-entry:hover .where,
.note-entry:hover .arrow { transition-duration: var(--signal-in); }

/* ---------- footer ----------
   The page ends by turning to night. A São Paulo skyline is cut out of the
   dark field so the silhouette IS the divider — the hairline rule that used to
   sit here is gone, and the buildings rise into the paper instead.

   It is a CSS mask rather than an <img> so the colour stays in the stylesheet.
   Baking it into the asset is the same trap the hats illustration fell into:
   an asset that silently depends on one exact page colour. */
.foot {
  --foot-ink: #16161b;
  /* How far the city sinks into the night.
     The near drawing stands on a ground bar of its own, 22 of its 245 units
     tall. Left above the field that bar is a second horizontal edge sitting
     right on top of the first, and the seam is the thing you see. Sunk by its
     own height it lands dark-on-dark and disappears, and the buildings meet
     the field directly.
     Written against the artwork's width because that is what sets its height,
     and because a percentage in a margin resolves against width anyway. */
  --sink: calc(min(100%, 940px) * 22 / 711);
  border-top: 0;
  /* The paper lives here rather than on the skyline, so the band the artwork
     is sunk into shows THROUGH it instead of being painted over. */
  background: var(--paper);
  padding: 0;
  /* The distant layer is allowed to run wider than the page on narrow screens,
     which is the only way it still stands above the near city there. Clipped
     here rather than on the skyline itself, so the overhang below is kept. */
  overflow: hidden;
}
/* Khevin's drawing, sitting on the paper with the dark field beginning exactly
   at its ground line — the ground bar in the artwork IS the top edge of the
   footer, so the two read as one thing and the hairline rule is not missed.

   It is PLACED rather than masked. The piece is drawn for a light sky: its
   silhouette is the dark and its windows are the light, so recolouring it to
   sit on the dark field turned it into a photographic negative — only the
   highlights survived and the buildings vanished. The one change made to the
   drawing is that its near-blacks were retuned to this footer's ink, so the
   ground bar meets the field without a seam. */
.foot-skyline {
  position: relative;
  line-height: 0;
  /* The overhang. The box stops at the city's ground line and the artwork
     keeps going, lying over the field below it. */
  margin-bottom: calc(var(--sink) * -1);
}
.foot-skyline img {
  display: block;
  height: auto;
}
.foot-skyline__near {
  position: relative;
  /* Capped and centred: it is a composition with palms at both ends, not a
     repeating band, and full-bleed at its own 2.9:1 would make the footer
     taller than some of the pages it closes. */
  width: min(100%, 940px);
  margin: 0 auto;
}
/* The second city, standing behind the first.
   Wider, plainer and lighter — the three things that read as distance. It is
   Khevin's other drawing reduced to one silhouette: flat tone, no windows, no
   shading, because detail at that remove is what makes two skylines look like
   clutter instead of depth. It stops at the field's edge rather than sinking
   with the near layer, since a pale grey lying on the night would show. */
.foot-skyline__far {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  width: min(126%, 1180px);
  /* It has to stop exactly where the night starts, or its pale ground bar lies
     on the dark field and reads as a smear across it.
     The offset is --sink, but it CANNOT be spent on `bottom`: a percentage
     there resolves against the container's height, and --sink is written in
     percentages of its width. That put it 19px too low. A margin resolves
     against width whichever edge it is on, so the same value lands right. */
  bottom: 0;
  margin-bottom: var(--sink);
}
/* Only the home page ends with the city — it is the last thing after the
   invitation to work together, and it earns that place by being the end of the
   argument rather than a band that repeats on ten pages. Everywhere else the
   footer is the dark field alone, and with no skyline to close the page it has
   to carry that weight itself, so it sits taller. */
.foot-body {
  background: var(--foot-ink);
  padding: clamp(14px, 1.5vw, 22px) 0 clamp(12px, 1.3vw, 19px);
}
/* Under the city it is a rule of night, not a slab: the skyline is doing the
   closing, and this only has to hold one line of type. The top clears the
   overhang BEFORE it starts measuring the gap, or the sink eats it. */
.foot-skyline + .foot-body {
  padding: calc(var(--sink) + clamp(16px, 1.7vw, 24px)) 0 clamp(15px, 1.5vw, 22px);
}
.foot .colophon { color: #8a8a93; }
.foot .colophon a { color: #b6b6be; }
.foot .colophon a:hover { color: var(--accent); }


/* ---------- signature ----------
   The asset is black artwork on transparency. On the Signal paper that is
   already right; the filter hook is here so it survives if this block is ever
   set on ink, since a PNG cannot take currentColor. */
.signature { opacity: .92; }
/* In the bio it signs off the paragraphs rather than heading a link list, so it
   wants air above it and none of the -4px pull that lines it up with a column
   of labels on the home page. */
.signature--bio {
  margin: 26px 0 30px;
}
.contact-block.is-ink .signature { filter: invert(1); opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  .work-list .item,
  .principle,
  .note-entry,
  .process-more { transition-duration: 1ms; }
}

/* Coarse pointers have no hover to animate out of, and a sticky :hover on a
   tapped row would leave it stranded in ink. */
@media (hover: none) {
  /* Rows sit on open paper, so transparent is their resting ground. */
  .work-list .item:hover,
  .note-entry:hover {
    --row-pad: 0px;
    background-color: transparent;
  }
  /* Cards do not: the principles grid paints its hairlines with its own black
     background, and .process-more sits on an explicit paper fill. Resetting
     either to transparent shows what is behind them. */
  .principle:hover,
  .process-more:hover { background-color: var(--paper); }
  .work-list .item:hover .title,
  .work-list .item:hover .title em,
  .note-entry:hover .title,
  .principle:hover h2 { color: var(--ink); }
  .work-list .item:hover .desc,
  .principle:hover p { color: var(--ink-soft); }
  .process-more:hover .lead { color: var(--ink); }
}

/* ---------- 06 process, on ink ----------
   The section turns over to the dark ground. Everything inside it was drawn for
   paper, so every value has to be answered rather than left to inherit — the
   greys in particular, which were picked for their drop BELOW paper and, left
   alone, either vanish into the field or shout off it. --ink-faintest is the
   loudest of those: a #c2c2c5 hairline is quiet on paper and a bright wire on
   ink.

   The ramp below mirrors the light one by luminance rather than by name, so the
   relationships hold: body copy sits about 7.6:1 on this field, which is where
   --ink-soft sits on paper, and the small labels land at 5.3:1, where
   --ink-faint does. The red is the page's own red, unlifted — a brightened one
   reads as a fourth colour, which is the mistake the roster headings already
   made once.

   #clients is dark too but on #1a1815, the warm near-black the bar illustration
   bakes into its counter. That one cannot move without breaking the merge, so
   the two darks differ by a hair. This follows the footer and the rest of the
   identity. */
#process {
  --dk-field: #16161b;
  --dk-ink: var(--paper);
  --dk-ink-2: #d2d2d8;   /* the lead under the headline */
  --dk-soft: #a8a8b0;    /* step body copy */
  --dk-faint: #8a8a93;   /* the uppercase step labels */
  --dk-rule: #34343c;    /* the hairline between steps */
  /* Same value as --dk-faint today, kept separate because it is a different
     job: one is text, one is structure, and either should be able to move
     without dragging the other with it. */
  --dk-line: #8a8a93;    /* the 2px structure: timeline, step edges, CTA */

  background: var(--dk-field);
  color: var(--dk-ink);
  /* The change of ground is the divider. A near-black hairline on a near-black
     field is not one. */
  border-bottom: 0;
}
#process .section-title { color: var(--dk-ink); }
#process .section-title em { color: var(--accent); }
#process .body-lg { color: var(--dk-ink-2); }

/* Red stops doing structural work here. On paper the timeline and the step
   edges are red and stay quiet, because red on paper is 4.6:1 and reads as a
   drawn line. On ink the same red sits at 3.4:1 but glows — hue carries
   attention that luminance alone does not — and six red strokes across the top
   became the loudest thing in the section, louder than the headline. So the
   structure goes neutral, at the same value as the step labels, and red is left
   to the things that actually signal: the numerals, the ems, and the hover.
   Swap --dk-line for var(--paper) to try it as white instead. */
#process .process::before { background: var(--dk-line); }
#process .process::after { border-left-color: var(--dk-line); }
#process .process .step {
  border-right-color: var(--dk-rule);
  border-left-color: var(--dk-line);
}
#process .process .step .top { color: var(--dk-faint); }
#process .process .step .top .big { color: var(--accent); }
#process .process .step h4 { color: var(--dk-ink); }
#process .process .step p { color: var(--dk-soft); }

/* The CTA keeps the identity's one interaction, mirrored. On paper the band
   rests on the page and inverts to ink under the pointer; here it rests on the
   ink and inverts to paper. Same move, same curve, same durations — the only
   thing that changes is which side it starts on. Leaving the light version in
   place would have given it nothing to invert TO.

   Its borders rest on the same neutral as the steps above it, so the shared
   border vocabulary styles.css built survives, and the red arrives on hover
   instead of sitting there — the band reaches back when you reach for it. */
#process .process-more {
  background: var(--dk-field);
  border-top-color: var(--dk-line);
  border-left-color: var(--dk-line);
  color: var(--dk-ink);
  transition:
    background-color var(--signal-out) var(--signal-curve),
    border-color var(--signal-out) var(--signal-curve);
}
#process .process-more .lead { color: var(--dk-ink); }
#process .process-more .lead em { color: var(--accent); }
#process .process-more .arrow { color: var(--dk-faint); }

#process .process-more:hover {
  background-color: var(--paper);
  border-top-color: var(--accent);
  border-left-color: var(--accent);
  transition-duration: var(--signal-in);
}
#process .process-more:hover .lead { color: var(--ink); }
#process .process-more:hover .lead em { color: var(--accent); }
#process .process-more:hover .arrow { color: var(--accent); }
#process .process-more:hover .lead,
#process .process-more:hover .lead em,
#process .process-more:hover .arrow { transition-duration: var(--signal-in); }

@media (hover: none) {
  /* No pointer to hover out of, so the band stays on its own ground rather than
     being stranded in paper after a tap. Mirrors the rule that does this for
     the light version. */
  #process .process-more:hover { background-color: var(--dk-field); }
  #process .process-more:hover .lead { color: var(--dk-ink); }
  #process .process-more:hover .arrow { color: var(--dk-faint); }
}


/* ---------- motion ----------
   The kinetic half of "kinetic editorial". PRODUCT.md bans motion as
   decoration even in this lane, so each of the three moments below is doing a
   job that the static page cannot do on its own:

     the hero      says READING ORDER — the lines arrive in the order they are
                   meant to be read, so the sentence completes itself rather
                   than landing all at once as a block of type.
     the sections  say ARRIVAL — a heading lifts as you reach it, which is the
                   same signal the nav gives by going red.
     the timeline  says SEQUENCE — the process rule draws left to right and each
                   step lights as it passes. 01 through 04 is an order in time,
                   and this is the one place on the site where the motion IS the
                   content rather than a comment on it.

   Two rules govern the whole layer:

   1. Nothing here may strand content invisible. The hero animates from CSS
      alone, so there is no script to fail. Everything else is armed by
      script.js and ONLY while it sits below the fold — if the script never
      runs, never finishes, or the observer never fires, the page is just the
      page. This site has made that mistake once already.
   2. Transform and opacity only. No layout properties, no bounce.

   The curve is DESIGN.md's ease-out. Long, heavily decelerated, no overshoot:
   things arrive and settle rather than snapping or springing. */
:root {
  --rise: cubic-bezier(0.16, 1, 0.3, 1);
  --rise-dur: 900ms;
  /* A line being drawn needs a different curve from a thing arriving.
     --rise is an ease-out-expo: it covers about 80% of the distance in the
     first fifth of its time and spends the rest settling. On something
     fading up that is exactly right, and you read it as arriving. On a line
     it means the line is simply THERE, and then creeps imperceptibly to
     full length — the travel, which is the whole point of a draw, never
     happens where the eye can catch it.
     This one is close to even, with soft ends: the stroke is legible the
     whole way across. */
  --draw: cubic-bezier(0.45, 0.12, 0.28, 1);
}

@keyframes rise {
  from { opacity: 0; transform: translateY(var(--rise-from, 14px)); }
  to   { opacity: 1; transform: none; }
}

/* ---- the hero, on load ----
   CSS only, deliberately. It is the one thing above the fold at first paint,
   so it cannot wait for a deferred script without either flashing into place
   or hiding content the script might never reveal. `both` holds the from-state
   through the delay so the cascade reads as one movement. */
.hero.v5 .h-block h1 .a,
.hero.v5 .h-block h1 .b,
.hero.v5 .h-block .right p,
.hero.v5 .dispatches .d {
  animation: rise var(--rise-dur) var(--rise) var(--rise-delay, 0ms) both;
}
.hero.v5 .h-block h1 .a { --rise-delay: 80ms; --rise-from: 18px; }
/* The second line carries the emphasis, so it comes from a little further and
   a beat later — the pause you would leave reading it aloud. */
.hero.v5 .h-block h1 .b { --rise-delay: 230ms; --rise-from: 22px; }
.hero.v5 .h-block .right p { --rise-delay: 420ms; }
.hero.v5 .dispatches .d:nth-child(1) { --rise-delay: 560ms; }
.hero.v5 .dispatches .d:nth-child(2) { --rise-delay: 650ms; }
.hero.v5 .dispatches .d:nth-child(3) { --rise-delay: 740ms; }

/* ---- and the rules that frame it ----
   The hero is drawn on four lines, and three of them were never in the
   cascade: they stood there at full strength while the words faded up past
   them, which is the tell that breaks the whole illusion — the frame cannot
   already be finished while its contents are still arriving.
   Each one now comes in just ahead of the thing it frames, a little quicker
   than the type so it settles rather than races it. The dispatch columns are
   not listed because their rules are borders on the cards themselves, and they
   have been fading with them all along.
   One token, --v5-rule, draws all of them, so the keyframe can name the end
   colour instead of hard-coding it. It is inherited from .hero.v5, and both
   elements here carry exactly one border, so animating border-color entire
   touches nothing else. */
/* They are drawn, not faded. A fade tells you a line has appeared; it does not
   tell you where it came from, and next to type that rises from somewhere it
   read as the frame blinking on. Each one now travels in from the outside of
   the page: the horizontals from the left edge of the window, well beyond the
   1400px the content sits in, and the uprights downward from the rule above
   them. Same idea as the process timeline, which is the one that works.

   Borders cannot be scaled, so each rule that is a border hands over to a
   pseudo-element in exactly its own place — same colour, same width, no
   layout moves. The base state is full size and the keyframe supplies the
   empty one, so with animations off the line is simply there. */
@keyframes draw-x { from { transform: scaleX(0); } to { transform: scaleX(1); } }
@keyframes draw-y { from { transform: scaleY(0); } to { transform: scaleY(1); } }

.hero.v5 { position: relative; border-bottom-color: transparent; }
.hero.v5 .h-block .right { position: relative; border-left-color: transparent; }
.hero.v5 .dispatches .d { border-right-color: transparent; }

.hero.v5::after,
.hero.v5 .h-block .right::before,
.hero.v5 .dispatches .d::after {
  content: "";
  position: absolute;
  background: var(--v5-rule);
  pointer-events: none;
}

/* the upright between the headline and the note beside it, first */
.hero.v5 .h-block .right::before {
  left: -1px;
  top: 0;
  bottom: 0;
  width: 1px;
  transform-origin: top center;
  animation: draw-y 850ms var(--draw) 300ms both;
}
/* the full-window line under the headline block. Re-hung off the left edge
   rather than centred by transform, so the transform is free to draw it. */
.hero.v5 .h-block::after {
  left: calc(50% - 50vw);
  transform: none;
  transform-origin: left center;
  animation: draw-x 1100ms var(--draw) 500ms both;
}
/* the dividers between the three dispatches, each just behind its card */
.hero.v5 .dispatches .d::after {
  right: -1px;
  top: 0;
  bottom: 0;
  width: 1px;
  transform-origin: top center;
  animation: draw-y 850ms var(--draw) var(--rule-delay, 600ms) both;
}
.hero.v5 .dispatches .d:nth-child(2) { --rule-delay: 690ms; }
.hero.v5 .dispatches .d:nth-child(3) { --rule-delay: 780ms; }
.hero.v5 .dispatches .d:last-child::after { display: none; }
/* and the one that closes the hero, last, under the three dispatches */
.hero.v5::after {
  bottom: -1px;
  left: calc(50% - 50vw);
  width: 100vw;
  height: 1px;
  transform-origin: left center;
  animation: draw-x 1100ms var(--draw) 820ms both;
}

/* Under 800px the layout stacks and both of those uprights become horizontal
   rules instead — so they draw sideways there, from the left. */
@media (max-width: 800px) {
  .hero.v5 .h-block .right { border-top-color: transparent; }
  .hero.v5 .dispatches .d { border-bottom-color: transparent; }
  .hero.v5 .h-block .right::before,
  .hero.v5 .dispatches .d::after {
    left: 0;
    right: 0;
    width: auto;
    height: 1px;
    transform-origin: left center;
    animation-name: draw-x;
  }
  .hero.v5 .h-block .right::before { top: -1px; bottom: auto; }
  .hero.v5 .dispatches .d::after { top: auto; bottom: -1px; }
}

/* ---- sections, on entry ----
   `.rise-armed` is added by script.js and only to elements that were below the
   fold when it looked. Anything already on screen is never hidden at all. */
.rise-armed {
  opacity: 0;
  transform: translateY(var(--rise-from, 14px));
}
.rise-armed.is-in {
  opacity: 1;
  transform: none;
  transition: opacity var(--rise-dur) var(--rise) var(--rise-delay, 0ms),
              transform var(--rise-dur) var(--rise) var(--rise-delay, 0ms);
}

/* ---- the process timeline ----
   The rule draws from the left. Its arrowhead is held back until the rule has
   very nearly arrived, so the arrow reads as the line landing rather than as a
   shape that was always there. */
/* The container is only ever marked, never hidden — it carries the rule and
   the arrowhead as pseudo-elements, and hiding it would take the steps with
   it. The steps arm normally. */
.js-motion .process::before {
  transform: scaleX(0);
  transform-origin: left center;
}
.js-motion .process.is-in::before {
  transform: scaleX(1);
  transition: transform 1150ms var(--draw);
}
.js-motion .process::after { opacity: 0; }
.js-motion .process.is-in::after {
  opacity: 1;
  transition: opacity 420ms linear 900ms;
}

/* The step rules draw too, instead of fading up with their card.
   The horizontal rule enters from outside the frame — it starts at -40px —
   and draws inward across the section. Its uprights do the same thing at
   ninety degrees: each one descends from the rule it hangs off into the block
   below, in step with the sweep passing over it. Fading them in was the odd
   one out, and next to a line that draws it read as the frame being switched
   on rather than made.

   They are borders, and a border cannot be scaled, so under motion the border
   goes colourless and its pseudo-element takes over in the same place — no
   layout moves, because the border keeps its width either way. The tones come
   from the tokens the section already sets, with the paper values as the
   fallback for anywhere this runs on light ground. */
/* Both selectors, because the section sets these same two colours at
   `#process .process .step` — an id outranks any number of classes, so the
   plain form alone lost and the borders never went colourless. */
.js-motion .process .step,
.js-motion #process .process .step {
  border-left-color: transparent;
  border-right-color: transparent;
}
.js-motion .process .step::before,
.js-motion .process .step::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  transform: scaleY(0);
  transform-origin: top center;
  pointer-events: none;
}
.js-motion .process .step::before {
  left: -2px;
  width: 2px;
  background: var(--dk-line, var(--accent));
}
.js-motion .process .step::after {
  right: -1px;
  width: 1px;
  background: var(--dk-rule, var(--ink-faintest));
}
/* the last column closes on the section, not on a rule of its own */
.js-motion .process .step:last-child::after { display: none; }
/* The transition lives on the arriving state, the way the horizontal rule's
   does. Declared on the base it would ease back out again whenever the class
   came off, which is both wrong and what made this look finished on its first
   test frame. */
.js-motion .process.is-in .step::before,
.js-motion .process.is-in .step::after {
  transform: scaleY(1);
  transition: transform 760ms var(--draw) var(--rise-delay, 0ms);
}

/* ---- the disciplines, on the same grammar ----
   Four columns under a rule, which is the timeline's shape exactly, so it gets
   the timeline's motion: the rule runs across and each upright drops behind it
   as it passes. Both are borders here too — the red one on top of the grid and
   the two on every card — so each hands over to a pseudo-element in its own
   place, full size by default so nothing is erased when motion is off.
   The right-hand rule changes at 900px, where the grid stacks and every card
   is ruled in red on both sides instead of hairlined on one. */
.js-motion .disciplines {
  position: relative;
  border-top-color: transparent;
}
.js-motion .disciplines .d {
  border-left-color: transparent;
  border-right-color: transparent;
}
.js-motion .disciplines::before,
.js-motion .disciplines .d::before,
.js-motion .disciplines .d::after {
  content: "";
  position: absolute;
  pointer-events: none;
}
.js-motion .disciplines::before {
  top: -2px;
  left: 0;
  right: 0;
  height: 2px;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: left center;
}
.js-motion .disciplines .d::before,
.js-motion .disciplines .d::after {
  top: 0;
  bottom: 0;
  transform: scaleY(0);
  transform-origin: top center;
}
.js-motion .disciplines .d::before { left: -2px; width: 2px; background: var(--accent); }
.js-motion .disciplines .d::after { right: -1px; width: 1px; background: var(--ink-faintest); }
.js-motion .disciplines .d:last-child::after { display: none; }

.js-motion .disciplines.is-in::before {
  transform: scaleX(1);
  transition: transform 1100ms var(--draw);
}
.js-motion .disciplines.is-in .d::before,
.js-motion .disciplines.is-in .d::after {
  transform: scaleY(1);
  transition: transform 780ms var(--draw) var(--rise-delay, 0ms);
}
.disciplines .d:nth-child(1) { --rise-delay: 160ms; }
.disciplines .d:nth-child(2) { --rise-delay: 380ms; }
.disciplines .d:nth-child(3) { --rise-delay: 600ms; }
.disciplines .d:nth-child(4) { --rise-delay: 820ms; }

@media (max-width: 900px) {
  /* stacked: every card is ruled red on both sides, and the last one keeps its
     right-hand rule instead of losing it */
  .js-motion .disciplines .d::after { width: 2px; right: -2px; background: var(--accent); }
  .js-motion .disciplines .d:last-child::after { display: block; }
}

/* Each step lights as the rule reaches it — the four delays are spaced to sit
   just behind the sweep rather than to a rhythm of their own. */
.process .step:nth-child(1) { --rise-delay: 180ms; }
.process .step:nth-child(2) { --rise-delay: 400ms; }
.process .step:nth-child(3) { --rise-delay: 620ms; }
.process .step:nth-child(4) { --rise-delay: 840ms; }

/* ---- the static fallback ----
   Reduced motion gets the finished page immediately. script.js also declines to
   arm anything, so the scroll layer never starts; this covers the hero, which
   runs without it. */
@media (prefers-reduced-motion: reduce) {
  /* Every rule's pseudo-element sits at full size by default, so switching the
     animation off leaves the line drawn rather than erased. */
  .hero.v5::after,
  .hero.v5 .h-block::after,
  .hero.v5 .h-block .right::before,
  .hero.v5 .dispatches .d::after,
  .hero.v5 .h-block h1 .a,
  .hero.v5 .h-block h1 .b,
  .hero.v5 .h-block .right p,
  .hero.v5 .dispatches .d { animation: none; }
  .rise-armed, .rise-armed.is-in { opacity: 1; transform: none; transition: none; }
  .js-motion .process::before { transform: none; }
  .js-motion .process::after { opacity: 1; }
}


/* ---------- the chapter break ----------
   Between the three case studies and the pieces that only earned a paragraph,
   the page changes register. It was marked by the same hairline every section
   ends with, sitting a few pixels under Amway's own row rule — two hairlines
   of the same weight, one meaning "next project" and the other meaning "next
   chapter", with nothing to tell you which was which.

   One heavy red rule, full bleed. Not a thicker version of the row rule: a
   different weight and a different colour, running the whole width of the
   window where the rows stop at the text measure. It is the only red
   horizontal on the page, which is what makes it read as the page turning.

   (It was drawn with a hairline companion under it, a printer's scotch rule,
   and opening from the centre. Both were wrong here — the companion read as a
   stray second rule rather than as part of one device, and opening from the
   centre made it the odd gesture out on a page where every rule draws from
   the left.)

   Drawn unconditionally, so with no script and no motion it is simply there;
   only its starting state is conditional. */
#work { border-bottom: 0; }
#work::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 5px;
  background: var(--accent);
  pointer-events: none;
}
.js-motion #work::after {
  transform: scaleX(0);
  transform-origin: left center;
}
.js-motion #work.is-in::after {
  transform: scaleX(1);
  transition: transform 1000ms var(--draw);
}


/* Clients: a red bar interior above a continuous near-black counter. */
#clients {
  background: #1a1815;
  color: #fcfdff;
  padding-top: 0;

  /* The roster below the counter is one black field split in two by a single
     line, so the air has to be symmetrical about that line.

     --roster-gap does the real work: equal above and below the rule.
     The field's TOP edge is not ours to set — the bar illustration ends in its
     own black counter surface, indistinguishable from this background, and that
     tail already reads as the top margin. Adding a margin under .s-head on top
     of it was the "weird padding": roughly 270px of nothing before the first
     roster. So the top edge is the illustration's, and --roster-foot is tuned
     to answer it at the bottom. */
  --roster-gap: clamp(40px, 4.4vw, 60px);
  --roster-foot: clamp(72px, 9vw, 128px);
  padding-bottom: var(--roster-foot);
}
#clients .s-head {
  width: 100vw;
  margin-left: calc(50% - 50vw);
  margin-right: calc(50% - 50vw);
  min-height: 600px;
  aspect-ratio: 2 / 1;
  /* Only the red lives on the element now. The illustration moved to its own
     layer below, so it can drift without taking the field with it. */
  background: #c93123;
  position: relative;
  overflow: hidden;
  /* styles.css puts 56px under every .s-head. Here that stacks on top of the
     illustration's own counter tail, which is the same black — so it reads as
     one oversized gap rather than as two separate margins. */
  margin-bottom: 0;
}
/* The bar itself. It arrives from the right as you reach the section — a drift
   of a few tens of pixels over a second and a half, which is enough to make the
   room feel entered rather than switched on, and little enough that you would
   not catch it looking for it.
   The image is exactly 2:1 inside a 2:1 box, so `cover` leaves no slack and
   background-position has nothing to move; it has to be a transformed layer.
   The left edge it uncovers is the same red as the field behind it, and the
   right edge is clipped, so nothing shows a seam. */
#clients .s-head::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  background: url('/assets/bar-signal.png') center bottom / cover no-repeat;
}
.js-motion #clients .s-head::before { transform: translateX(2.6%); }
.js-motion #clients .s-head.is-in::before {
  transform: none;
  transition: transform 1500ms var(--rise);
}
#clients .s-head > div {
  /* above the illustration layer */
  position: relative;
  z-index: 1;
  max-width: var(--max);
  margin: 0 auto;
  padding: clamp(64px, 6vw, 104px) var(--gutter) 100px;
}
#clients .section-title {
  color: #fcfdff;
  max-width: 62%;
  text-wrap: initial;
}
#clients .section-title em { color: #1a1815; }
#clients .glass-art { display: none; }
#clients .body-lg { color: #fcfdff; max-width: 52%; }
/* No rule above the first roster — the counter edge already closes the red bar,
   and a second line there made the lower rule read as a lid on the bottom half
   rather than as the thing dividing the two. Only the second strip is ruled. */
#clients .clients-strip {
  border-top: 0;
  padding-top: 0;
}
#clients .clients-strip--next {
  border-top: 1px solid #514b44;
  margin-top: var(--roster-gap);
  padding-top: var(--roster-gap);
}
#clients .clients-strip .col-l h3 { color: #fcfdff; }
/* The roster headings take the page's own red, not a tint of it — the pale
   salmon read as a fourth colour and went chalky against the counter. */
#clients .clients-strip .col-l h3 em { color: var(--accent); }
#clients .client-strip-img img { filter: brightness(0) invert(1); opacity: .9; }
@media (min-width: 1800px) {
  #clients .s-head { aspect-ratio: auto; min-height: 780px; background-size: 100% 100%; }
}
@media (max-width: 900px) {
  #clients .s-head { min-height: 560px; }
  #clients .s-head > div { padding-top: 60px; }
  #clients .section-title { font-size: clamp(40px, 5.6vw, 58px); }
  #clients .body-lg { font-size: 18px; }
}
@media (max-width: 600px) {
  /* The copy sits ON the illustration here, the same as it does on desktop —
     the artwork was drawn with its left 62% deliberately empty for exactly
     this. It used to be detached into an ::after band below the text, which
     read as two separate blocks stacked up.

     `100% auto` anchored bottom-left is what makes it work: the art keeps its
     own 2:1 ratio at full width along the bottom, so the glass and the counter
     stay whole, and the red field above simply grows to whatever the text
     needs. `cover` cannot do this — at a phone's ratio it would crop the empty
     left zone and the vignette both. */
  #clients .s-head {
    min-height: 0;
    aspect-ratio: auto;
    background-size: 100% auto;
    background-position: left bottom;
  }
  /* The bottom padding is the counter: the art's lowest band runs black across
     the full width, so the copy has to finish above it or it sets black on
     black. 30vw ≈ 60% of the art's height at any phone size. */
  #clients .s-head > div { padding: 44px var(--gutter) 30vw; }
  #clients .section-title {
    /* Inside the empty zone, with the gutters already taken off. Sized so
       "Small rooms." very nearly fills that column — the headline carries the
       art here, and at the old size it was only reaching 55% of the width.
       It stays three lines: the copy has hard breaks, and the only way to a
       fourth is wrapping "Small rooms.", which strands "Small" on its own. */
    max-width: 66%;
    /* Sized off the COLUMN, not the viewport. The column is 66% of
       (100vw - 2 gutters), so `9.5vw - 6px` tracks it and the headline fills a
       steady ~94% of the measure at every phone size. A plain vw value drifts,
       because the fixed gutters are a bigger share of a small screen — and
       font-size-adjust scales the used size up from the nominal one, so the
       margin has to be real rather than eyeballed. */
    font-size: clamp(26px, calc(9.5vw - 6px), 46px);
  }
  #clients .body-lg { max-width: 66%; font-size: 16px; }
}
@media (max-width: 330px) {
  /* The headline size bottoms out at 26px, so below roughly this width the 66%
     column can no longer hold "Small rooms." and it breaks mid-headline. Giving
     back some of the empty zone is the lesser cost — there is no vignette to
     collide with this far left. */
  #clients .section-title,
  #clients .body-lg { max-width: 74%; }
}
