/* ==========================================================================
   home.css — the home page only. Loaded third, after tokens.css and site.css.

   Sections, in the order the design spec fixes:
     preloader · hero · marquee · story · work · impact · experience · speaking
   The ContactFooter is shared chrome and lives in site.css.
   ========================================================================== */

/* --- Preloader ------------------------------------------------------------ */
/* Shown only by home.mjs, which is also the only thing that removes it — so a
   script that never runs cannot leave a cover over the page (AC-26). It is
   hidden outright under reduced motion (AC-21). */
.preloader {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  padding: var(--gutter);
  background: var(--background);
  transform: translateY(0);
  transition: transform var(--t-wipe) var(--ease-wipe);
}
.preloader[hidden] { display: none; }
.preloader.is-wiping { transform: translateY(-100%); }
html.preloading { overflow: hidden; }

.preloader__row { display: flex; justify-content: space-between; gap: 1rem; }

.preloader__label {
  font-size: var(--fs-eyebrow);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: var(--track-eyebrow);
  color: var(--muted);
  transition: opacity var(--t-hover) var(--ease-out);
}
.preloader.is-fading .preloader__label { opacity: 0; }

.preloader__count {
  align-self: flex-end;
  font-family: var(--font-display);
  font-size: var(--fs-display);
  line-height: 1;
  color: var(--accent);
  transition: opacity var(--t-hover) var(--ease-out);
}
.preloader.is-fading .preloader__count { opacity: 0; }

@media (prefers-reduced-motion: reduce) {
  .preloader { display: none !important; }
}

/* --- Hero ----------------------------------------------------------------- */
/* The giant name is absolute at the bottom and behind the portrait column.
   --name-fs is mega, dropping to whatever fits the viewport below 640px: the
   spec's "hero at sm" would put a seven-character line 25% wider than a 375px
   screen, and 00-shared.md AC-17 requires both lines to fit without wrapping
   and without being cut off. 3.25 is Anton's measured advance for the longer
   line (KORNYEV, 7 characters) in ems. */
.hero {
  position: relative;
  min-height: 100vh;
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  padding-top: calc(var(--nav-h) + 3rem);
  /* The fit term is a safety net at every width, not only on a phone: with
     `white-space: nowrap` the only other outcome is a line running off the
     screen, which AC-17 forbids. Above 640px it never binds. */
  --name-fs: min(var(--fs-mega), calc((100vw - (var(--gutter) * 2)) / 3.25));
  --name-block: calc(var(--name-fs) * var(--lh-mega) * 2);
}

@media (max-width: 640px) {
  .hero { --name-fs: min(var(--fs-hero), calc((100vw - (var(--gutter) * 2)) / 3.25)); }
}

.hero__inner {
  position: relative;
  z-index: 3;
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 2.5rem;
  padding-bottom: calc(var(--name-block) + 1rem);
}

/* THE TEXT CARRIES ITS OWN GROUND when there is a photograph behind it.
   Measured before this existed, as the worst contrast between a glyph and the
   pixel under it, cream #f3f1ea on the composited backdrop:

     1920   8.68:1   2 of 10 lines cross the portrait
     1440   1.36:1   3 of 10
     1024   1.12:1   4 of 10
      768   1.05:1   8 of 10
      375   1.00:1   11 of 12   — the roles and the email are invisible

   The design spec's hero is a centred 86vh portrait with the text in front of
   it, and that works because the spec's asset is a cutout on nothing: the
   text only ever crosses a silhouette. This photograph is a room, and the
   bright parts of it — a white shirt cuff, a lit wall — land under the middle
   of the page's main sentence. No mask on the picture fixes that, because at
   375px the text covers the whole picture; there is no edge to fade.

   So the scrim is sized to the TEXT, not to the picture, and it costs nothing
   where the two do not meet. It is one text column wide, so on a wide screen
   it is a band on the left and the portrait is untouched; on a phone the text
   column IS the viewport, so it covers the width — which is the honest
   outcome, because there the words and his face are in the same place and the
   words are what the page is for. It stops before the giant name, which is
   meant to be behind him.

   92% rather than a flat fill: that is what brings a white pixel (255) to
   about 28, and cream on 28 is over 13:1 — the margin is deliberate, because
   the brightest pixel in the frame is not the brightest pixel a replacement
   photograph might have. */
.hero[data-portrait] .hero__inner::before {
  content: "";
  position: absolute;
  z-index: -1;
  /* THE FADE IS A LENGTH, NOT A PERCENTAGE, and that is the whole trick. The
     inner's padding-bottom is the name block plus 1rem, so the last line of
     text — the email and SCROLL — ends exactly that far above the inner's
     edge. Pulling the box up by (name block − 6rem) puts its bottom 7rem
     below that line at every width, and a 6rem fade then runs entirely in the
     gap. A percentage fade cannot do this: the name block is 181px at 375 and
     358px at 1440, so the same percentage lands above the text at one width
     and below it at the other. It did: the email measured 3.3:1 at 375
     because the fade had already started under it.

     95%, and the binding case is that email. It is --muted, which is 4.98:1
     against the background on its own — there is almost no headroom in it at
     all, and the backdrop under it has to come down to about 24/255 for
     4.5:1. 93% measured 4.43 there and 90% measured 3.30. The cream body text
     needs nothing like this; the one muted line sets the number. */
  inset: calc(var(--nav-h) * -1) calc(var(--gutter) * -1)
         calc(var(--name-block) - 6rem) calc(var(--gutter) * -1);
  background: color-mix(in srgb, var(--background) var(--hero-scrim), transparent);
  mask-image: linear-gradient(to bottom, #000 calc(100% - 6rem), transparent 100%);
  -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 6rem), transparent 100%);
  pointer-events: none;
}

/* NO HORIZONTAL FADE BELOW 1024. The fade is worth having only where the text
   genuinely leaves the right of the screen free, and below 1024 it does not:
   the portrait is 48vw or 80vw, and .hero__foot is a space-between row, so
   SCROLL sits hard against the right edge with the picture behind it. A fade
   tuned to the body column leaves exactly that word unprotected — it measured
   1.66:1 before this split. From 1024 the portrait is 36vw and the text is a
   band on the left, so the fade costs the picture nothing and gives back the
   whole right-hand side of it at full strength. */
@media (min-width: 1024px) {
  .hero[data-portrait] .hero__inner::before {
    /* The same length trick as the vertical fade, for the same reason. The
       box starts one gutter left of the page and the body column is 30rem
       inside it, so (2 gutters + 31rem) covers every line with 1rem to spare
       at any root size, and the 4rem on top is the fade — which therefore
       begins only after the text has ended, rather than at a percentage that
       lands inside it. It did: a 62% stop put the right-hand end of the body
       paragraph at 61% strength, which the dark theme survived at 6.23:1 and
       the light theme did not, at 1.89:1. */
    width: min(100% + var(--gutter) * 2, calc(var(--gutter) * 2 + 35rem));
    background: linear-gradient(to right,
      color-mix(in srgb, var(--background) var(--hero-scrim), transparent) 0,
      color-mix(in srgb, var(--background) var(--hero-scrim), transparent) calc(100% - 4rem),
      transparent 100%);
  }
}

.hero__text { display: flex; flex-direction: column; gap: 2rem; max-width: 46rem; }

.hero__roles {
  display: flex;
  flex-direction: column;
  gap: .25rem;
  font-size: var(--fs-h3);
  line-height: var(--lh-h3);
  font-weight: 500;
  --rv-dur: var(--t-roles);
  --rv-stagger: var(--stagger-roles);
}
.hero__roles li:nth-child(n+2) { color: var(--muted); }

.hero__body {
  font-size: var(--fs-lead);
  line-height: var(--lh-lead);
  max-width: 40rem;
  --rv-dur: var(--t-body);
}

/* 30rem when a photograph is behind it, from 1024 up. 40rem sets this
   paragraph about 80 characters to the line, which is past the comfortable
   measure anyway; 30rem is about 60. The reason to do it here is that those
   last two inches of line are what reach across the portrait: at 1440 the
   paragraph ended at x=680 and he starts at 597, so the scrim protecting the
   text had to wash out a third of him. At 30rem it ends at 520 and the scrim
   clears before he does. Scoped to [data-portrait] so the hero without a
   photograph — which is what AC-H1 guarantees still composes — keeps the
   width the design gives it. */
@media (min-width: 1024px) {
  .hero[data-portrait] .hero__body { max-width: 30rem; }
}

.hero__foot {
  margin-top: auto;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1.5rem;
  flex-wrap: wrap;
  font-size: var(--fs-eyebrow);
  text-transform: uppercase;
  letter-spacing: var(--track-eyebrow-tight);
  color: var(--muted);
}
.hero__foot a { text-transform: none; letter-spacing: normal; font-size: var(--fs-body); }
.hero__cue { display: inline-flex; align-items: center; gap: .6rem; }
.hero__cue::after {
  content: "";
  width: 1px;
  height: 2.5rem;
  background: linear-gradient(to bottom, var(--line-strong), transparent);
}

.hero__name {
  position: absolute;
  left: 50%;
  bottom: 0;
  z-index: 1;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: var(--name-fs);
  line-height: var(--lh-mega);
  letter-spacing: var(--track-heading);
  text-transform: uppercase;
  text-align: center;
  white-space: nowrap;
  color: var(--accent);
  --rv-dur: var(--t-name);
  --rv-stagger: var(--stagger-letter);
}
/* The 6% offset only has a point when there is a portrait in front of it. */
.hero[data-portrait] .hero__name { transform: translateX(-50%) translateY(6%); }

.hero__name-line { display: block; }

/* The portrait column, when a photograph exists. Bare mode: no veil, no
   sweep, no vignette, no placeholder — so with no photograph the hero reads
   as a finished composition rather than an empty frame (AC-H1). The gradient
   base is part of this block and absent with it, because over an empty hero it
   would only fade out the bottom of the giant name. */
.hero__portrait {
  position: absolute;
  bottom: 0;
  left: 50%;
  transform: translateX(-50%);
  z-index: 2;
  height: 86vh;
  width: 80vw;

  /* THE EDGES ARE FEATHERED, and they have to be. The slot was written for a
     photograph and .liquid--bare turns the vignette off, because
     .hero__gradient was meant to be the vignette — but that gradient is only
     at the bottom, and the picture that arrived is not a cutout on a plain
     backdrop. It is a room: a wood wall, a door frame, a lectern. Unmasked,
     the wall stops dead at three straight lines and the hero reads as a
     rectangle pasted over the page rather than a composition, with the body
     text crossing a hard edge mid-sentence.

     A mask, not a crop: the file is untouched and so is the framing, which is
     the rule the photographs on this site are held to. Two gradients
     intersected — sides first, then the top — so the bottom is left alone for
     .hero__gradient, which also spans the full page width and so is doing a
     different job than a mask could. mask-composite: intersect is the
     standard property; a renderer too old for it falls back to the first
     gradient alone, which is the horizontal fade, which is the one that
     matters most. */
  mask-image:
    linear-gradient(to right, transparent, #000 18%, #000 82%, transparent),
    linear-gradient(to bottom, transparent, #000 14%);
  mask-composite: intersect;
  -webkit-mask-image:
    linear-gradient(to right, transparent, #000 18%, #000 82%, transparent),
    linear-gradient(to bottom, transparent, #000 14%);
  -webkit-mask-composite: source-in;
}
@media (min-width: 640px) { .hero__portrait { width: 48vw; } }
@media (min-width: 1024px) { .hero__portrait { width: 36vw; } }
/* `top`, not `bottom`. The slot is 86vh tall against 36/48/80vw, so it is
   normally NARROWER than the photograph's 3:4 and `cover` crops horizontally,
   where the vertical anchor does nothing. It only takes effect on a window
   wide enough to invert that — an ultrawide (2560x1080 crops 24% vertically),
   a landscape tablet, a phone turned sideways — and there `bottom` would take
   the crop off the top, which is where his head is. `top` takes it off the
   floor instead, and the floor is the part .hero__gradient is already fading
   out. Measured: the slot's ratio passes 3:4 at a viewport ratio of 1.79 on
   desktop, 1.34 on tablet and 0.81 on a phone. */
.hero__portrait .liquid__img { object-position: center top; }

/* THE LIGHT THEME DROPS THE PORTRAIT BELOW 1024, and this is the one place
   the two themes show different content rather than different colours.

   The reason is in --hero-scrim (tokens.css): on the cream page the scrim over
   the text has to be opaque, because this photograph is mostly dark and dark
   text on any of it fails. Above 1024 that costs nothing — the text is a band
   on the left and he stands clear of it to the right. Below 1024 the text is
   the whole width, so an opaque scrim covers everything except the strip under
   the last line, and the only part of the picture left showing is his shoes on
   the floor. A pale photograph of shoes is worse than no photograph, and AC-H1
   already guarantees the hero composes without one — it is the state the page
   shipped in until now, and liquid.mjs puts it in that state anyway whenever
   the file fails to load.

   Both ways light can be on, exactly as tokens.css does it: the stored choice,
   and the system preference with no stored choice. The name's 6% offset goes
   with the portrait, because it exists to make room behind him. */
@media (max-width: 1023px) {
  :root[data-theme="light"] .hero__portrait,
  :root[data-theme="light"] .hero__gradient,
  :root[data-theme="light"] .hero[data-portrait] .hero__inner::before { display: none; }
  :root[data-theme="light"] .hero[data-portrait] .hero__name { transform: translateX(-50%); }
}
@media (max-width: 1023px) and (prefers-color-scheme: light) {
  :root:not([data-theme="dark"]) .hero__portrait,
  :root:not([data-theme="dark"]) .hero__gradient,
  :root:not([data-theme="dark"]) .hero[data-portrait] .hero__inner::before { display: none; }
  :root:not([data-theme="dark"]) .hero[data-portrait] .hero__name { transform: translateX(-50%); }
}

.hero__gradient {
  position: absolute;
  inset-inline: 0;
  bottom: 0;
  z-index: 2;
  height: 26vh;
  background: linear-gradient(to bottom, transparent, var(--background));
}

/* The whole hero fits one screen on a phone, the giant name included: the
   name is the identification, and AC-H2 wants it in the first screen. These
   sit at the end of the hero block on purpose — same specificity as the rules
   above, so order is what decides. */
@media (max-width: 640px) {
  .hero { padding-top: calc(var(--nav-h) + 1rem); }
  .hero__inner { gap: 1.25rem; padding-bottom: calc(var(--name-block) + .75rem); }
  .hero__text { gap: 1rem; }
  .hero__roles { font-size: var(--fs-lead); line-height: var(--lh-lead); }
  .hero__body { font-size: var(--fs-body); line-height: var(--lh-body); }
  .hero__foot { gap: 1rem; }
  .hero__cue::after { height: 1.5rem; }
}

/* --- Marquee -------------------------------------------------------------- */
/* Doubled track, 0 → -50%, 22s linear. Stopped under reduced motion, where it
   rests on a phrase boundary and reads as a deliberate static band (AC-H11).
   It carries nothing that is not also readable at rest (AC-H9). */
.marquee {
  border-block: 1px solid var(--line);
  padding-block: 1.25rem;
  overflow: hidden;
}

.marquee__track {
  display: flex;
  width: max-content;
  animation: marquee 22s linear infinite;
}

.marquee__group { display: flex; }

.marquee__item {
  display: flex;
  align-items: center;
  gap: 2.5rem;
  padding-right: 2.5rem;
  font-family: var(--font-display);
  font-size: var(--fs-h3);
  line-height: 1;
  letter-spacing: .02em;
  text-transform: uppercase;
  color: var(--muted);
}
.marquee__item::after {
  content: "";
  width: .4rem; height: .4rem;
  border-radius: 9999px;
  background: var(--accent);
}

@keyframes marquee {
  from { transform: translateX(0); }
  to { transform: translateX(-50%); }
}

@media (prefers-reduced-motion: reduce) {
  .marquee__track { animation: none !important; }
}

/* --- Story ---------------------------------------------------------------- */
.story__body {
  max-width: 44rem;
  margin-top: 3rem;
  font-size: var(--fs-lead);
  line-height: var(--lh-lead);
}

.principles {
  margin-top: 4rem;
  display: grid;
  gap: 1px;
  background: var(--line);
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
  --rv-dur: var(--t-list);
  --rv-stagger: 110ms;
}
@media (min-width: 768px) { .principles { grid-template-columns: repeat(3, 1fr); } }

.principle {
  background: var(--background);
  padding: 2.5rem 2rem;
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.principle__n {
  font-size: var(--fs-eyebrow);
  font-weight: 500;
  letter-spacing: var(--track-stat);
  color: var(--accent);
}
.principle__t { font-size: var(--fs-h3); line-height: var(--lh-h3); font-weight: 600; }
.principle p { color: var(--muted); }

/* --- Work ----------------------------------------------------------------- */
.work__grid {
  margin-top: 4rem;
  display: grid;
  gap: 2.5rem;
  --rv-dur: var(--t-card);
  --rv-stagger: 120ms;
}
@media (min-width: 768px) { .work__grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1024px) { .work__grid { grid-template-columns: repeat(3, 1fr); } }

.card { display: flex; flex-direction: column; gap: 1.25rem; }
.card .liquid { aspect-ratio: 4 / 3; }

/* --- The work cards' artwork ---------------------------------------------- */
/* Five backgrounds, in gradients only: no file, no request, no <img>, and
   nothing added to the CSP. The alternative was five generated images, which
   were tried and measured and came back at mean brightness 98, 73 and 58 out
   of 255 — two to five times too bright for a card carrying light text — at
   about 1.5 MB each. These weigh nothing and cannot drift from the accent,
   because they ARE the accent: every mark is a color-mix of var(--accent),
   and the base is the same surface-to-background wash .liquid--empty already
   uses, so both themes follow the tokens without a second set of values.

   PLACEHOLDER MODE IS UNTOUCHED. The figures keep .liquid--empty and gain
   .card__art, which lives here in home.css and nowhere else — /events, which
   relies on placeholder mode for an event with no photograph, loads a
   different stylesheet and sees none of this. Remove home.css and these cards
   fall back to the branded placeholder exactly as they did before.

   HOW DARK, AND WHY THAT NUMBER. The target is that --foreground text clears
   7:1 — WCAG AAA for small text — over the brightest pixel any of these can
   produce, not over their average. The bound is the strongest mark, --art-1,
   composited over the lightest point of the base: in dark that is about
   8:1, in light about 9:1. It is checked rather than asserted, by reading the
   resolved colours back out of the browser and compositing them; see the
   comment on --art-1. The vignette over the lower half only ever helps, so it
   is left out of the bound.

   STATIC. Nothing here animates, so there is nothing for the reduced-motion
   block to switch off. These figures carry no .liquid__veil or .liquid__sweep
   in the markup either, so the component's hover transitions have nothing to
   act on — the cards are the same at rest, on hover and in print. */
.card__art {
  /* The three weights the whole set is drawn in, and the ceiling is --art-1.
     Two things set it, and both were found by measuring rather than by eye.

     A FLOOR: the first pass used 38/22/12 and the cards rendered as flat
     black rectangles. A 1px line at 12% of anything over #08080a is not a
     line.

     A CEILING: what matters is not the brightest single mark but the
     brightest OVERLAP, because these are stacked gradient layers and a node
     can land exactly where two paths cross. At 40/28/17 that stacking put the
     agents card at 4.87:1 — still AA, but not what the comment above it
     claimed.

     Hence the rule that follows from it: --art-1 is only ever used for a mark
     that CANNOT sit on another one. The leading ray, the first spine, the
     selected dot — one mark per card, each on a layer of its own. Anything
     tiled or repeated is --art-2 or --art-3, because a tiled dot will land on
     a crossing path eventually. That rule, not a smaller number, is what
     brought the two worst cards back over the line; lowering the ceiling
     enough to make overlapping --art-1 safe would have dimmed the other three
     for nothing.

     With it, the worst overlap on every card is at or above 7:1 — WCAG AAA
     for small text — in both themes. Reproduce it by reading the resolved
     colours back out of getComputedStyle and compositing every layer's
     brightest stop over the base.

     Raising any of these is the one change here that has to be re-measured,
     and the light theme fails first — there a strong accent mark DARKENS a
     cream card and eats the dark label's contrast from the other side. */
  --art-1: color-mix(in srgb, var(--accent) 34%, transparent);
  --art-2: color-mix(in srgb, var(--accent) 24%, transparent);
  --art-3: color-mix(in srgb, var(--accent) 15%, transparent);
  /* The wash .liquid--empty paints, restated because a background-image stack
     replaces the shorthand it sets rather than layering under it, and tinted
     12% toward the accent so an unmarked corner still belongs to the set
     rather than reading as a hole in the page. */
  --art-base: radial-gradient(130% 130% at 30% 15%,
    color-mix(in srgb, var(--accent) 12%, var(--surface-2)), var(--background));
}

/* The label is the card's own title and it sits on the artwork now, so it
   takes the foreground colour rather than --muted. --muted is 4.98:1 against
   the plain background before any artwork is behind it, which leaves nothing
   to spend; --foreground starts at 17.7:1 and can afford the tint. */
.card__art .liquid__label { color: var(--foreground); }

/* 1 · AI Competence Center — six streams meeting at one point. Six rays,
   because six is the number in the sentence beside it: delivery,
   architecture, business analysis, QA, development, DevOps. The origin is
   below the bottom-left corner so they read as converging on something
   off-frame rather than radiating from the middle of the card. In CSS conic
   0deg points up and turns clockwise, so from an origin below-left the card
   lies between about 17deg and 65deg; the first attempt swept 200–227deg,
   which points away from the card and drew nothing at all. */
.card__art--portfolio {
  background-image:
    conic-gradient(from 0deg at 8% 122%,
      transparent 0deg 17deg,
      var(--art-1) 17deg 20deg, transparent 20deg 26deg,
      var(--art-2) 26deg 29deg, transparent 29deg 35deg,
      var(--art-3) 35deg 38deg, transparent 38deg 44deg,
      var(--art-2) 44deg 47deg, transparent 47deg 53deg,
      var(--art-3) 53deg 56deg, transparent 56deg 62deg,
      var(--art-2) 62deg 65deg, transparent 65deg 360deg),
    var(--art-base);
}

/* 2 · Agents in enterprise tools — a lattice of nodes with two paths cut
   across it. The nodes tile, so the field continues past the frame rather
   than being a diagram that happens to fit. */
.card__art--agents {
  background-image:
    radial-gradient(circle at 50% 50%, var(--art-2) 0 2.5px, transparent 3px),
    linear-gradient(26deg, transparent 48.9%, var(--art-3) 48.9% 51.1%, transparent 51.1%),
    linear-gradient(-26deg, transparent 48.9%, var(--art-3) 48.9% 51.1%, transparent 51.1%),
    var(--art-base);
  background-size: 2.5rem 2.5rem, 100% 100%, 100% 100%, 100% 100%;
}

/* 3 · Skills and prompts, shared — spines on a shelf, of uneven width,
   because a library of 20 skills and 50 prompts is not a uniform thing. The
   period is 56px rather than the 34px it started at: at the shorter one the
   card read as a barcode and was the loudest of the five, which broke the
   restraint the set depends on to look like a set. */
.card__art--library {
  background-image:
    repeating-linear-gradient(to right,
      var(--art-1) 0 5px,     transparent 5px 19px,
      var(--art-3) 19px 24px, transparent 24px 34px,
      var(--art-2) 34px 37px, transparent 37px 56px),
    var(--art-base);
}

/* 4 · Role-based AI enablement — one source, reaching outward. The origin is
   on the left edge, so the rings arrive across the card rather than sitting
   in the middle of it. */
.card__art--roles {
  background-image:
    radial-gradient(circle at -4% 50%, var(--art-2) 0 9px, transparent 11px),
    repeating-radial-gradient(circle at -4% 50%,
      transparent 0 22px, var(--art-3) 22px 24px,
      transparent 24px 44px, var(--art-2) 44px 46px),
    var(--art-base);
}

/* 5 · Employee-discount portal — a field of candidates with one picked out.
   The field is faint and tiled; the selection is the only --art-1 mark on the
   card, which is the whole point of it. */
.card__art--select {
  background-image:
    radial-gradient(circle at 68% 38%, var(--art-1) 0 11px, transparent 13px),
    radial-gradient(circle at 50% 50%, var(--art-3) 0 2.5px, transparent 3px),
    var(--art-base);
  background-size: 100% 100%, 2.25rem 2.25rem, 100% 100%;
}
.card__title { font-size: var(--fs-h3); line-height: var(--lh-h3); font-weight: 600; }
.card__meta {
  font-size: var(--fs-eyebrow);
  letter-spacing: var(--track-eyebrow-tight);
  text-transform: uppercase;
  color: var(--muted);
}
.card p { color: var(--muted); }

/* --- Impact --------------------------------------------------------------- */
.stats {
  margin-top: 4rem;
  display: grid;
  gap: 1px;
  background: var(--line);
  border: 1px solid var(--line);
  --rv-dur: var(--t-list);
  --rv-stagger: 110ms;
}
@media (min-width: 640px) { .stats { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1024px) { .stats { grid-template-columns: repeat(4, 1fr); } }

.stat {
  background: var(--background);
  padding: 2.5rem 2rem;
  display: flex;
  flex-direction: column;
  gap: .75rem;
  transition: color var(--t-hover) var(--ease-out);
}
/* The accent shift on hover carries no information (AC-H29). */
.stat:hover { color: var(--accent); }

.stat__value {
  font-family: var(--font-display);
  font-size: var(--fs-h1);
  line-height: var(--lh-h1);
  letter-spacing: var(--track-heading);
}
.stat__label {
  font-size: var(--fs-eyebrow);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: var(--track-stat);
}
/* Every figure carries its employer and period in the block a visitor reads
   as one unit (AC-H23, 00-shared.md AC-35). */
.stat__source {
  margin-top: auto;
  font-size: var(--fs-eyebrow);
  letter-spacing: var(--track-eyebrow-tight);
  text-transform: uppercase;
  color: var(--muted);
}

/* --- Experience ----------------------------------------------------------- */
.roles {
  margin-top: 4rem;
  display: flex;
  flex-direction: column;
  --rv-dur: var(--t-list);
  --rv-stagger: 110ms;
}

.role {
  display: grid;
  gap: .5rem 2rem;
  padding-block: 2rem;
  border-top: 1px solid var(--line);
}
@media (min-width: 768px) {
  .role { grid-template-columns: 14rem 1fr; align-items: baseline; }
}
.role:last-child { border-bottom: 1px solid var(--line); }

.role__when {
  font-size: var(--fs-eyebrow);
  letter-spacing: var(--track-eyebrow-tight);
  text-transform: uppercase;
  color: var(--muted);
}
.role__title { font-size: var(--fs-h3); line-height: var(--lh-h3); font-weight: 600; }
.role__org { color: var(--muted); }
.role p { color: var(--muted); }

/* The three Andersen titles are one progression, not three unrelated jobs. */
.role--group .role__steps { display: flex; flex-direction: column; gap: 1.25rem; margin-top: 1rem; }
.role__step { display: flex; gap: 1rem; align-items: baseline; }
.role__step-when {
  flex: none;
  min-width: 9rem;
  font-size: var(--fs-eyebrow);
  letter-spacing: var(--track-eyebrow-tight);
  text-transform: uppercase;
  color: var(--muted);
}

/* Study ran alongside two of the jobs. Its own row, visually apart, so the
   overlapping dates read as study beside work rather than as a contradiction
   in one timeline (AC-H33). */
.role--study {
  margin-top: 2.5rem;
  border: 1px solid var(--line);
  border-left: 2px solid var(--accent);
  padding-inline: 2rem;
}
.role--study:last-child { border-bottom: 1px solid var(--line); }

/* --- Speaking ------------------------------------------------------------- */
.speaking__layout {
  margin-top: 4rem;
  display: grid;
  gap: 3rem;
  align-items: start;
}
@media (min-width: 1024px) { .speaking__layout { grid-template-columns: 1fr 22rem; gap: 5rem; } }

.speaking__standfirst { max-width: 44rem; font-size: var(--fs-lead); line-height: var(--lh-lead); }

.speaking__statements { margin-top: 3rem; display: grid; gap: 2.5rem; }
.speaking__item { --rv-dur: var(--t-word); --rv-stagger: var(--stagger-word); --rv-ease: var(--ease-quart); }

/* With scripts the statements rotate in one place, and the block holds its
   height so a change never moves the page (AC-H40). Without scripts all four
   are simply there to read, which is the point of keeping them in the HTML. */
html.js .speaking__statements { min-height: 18rem; gap: 0; }
html.js .speaking__item { grid-area: 1 / 1; }
html.js .speaking__item:not(.is-active) { visibility: hidden; }

.speaking__quote {
  font-family: var(--font-display);
  font-size: var(--fs-h3);
  line-height: 1.25;
  letter-spacing: var(--track-heading);
  text-transform: uppercase;
}
@media (min-width: 1024px) { .speaking__quote { font-size: 2.5rem; line-height: 1.15; } }
/* The spec translates each word 18px rather than a full line height. */
html.js [data-reveal] .speaking__quote .rv-word > span { transform: translateY(18px); }

.speaking__by {
  margin-top: 1.5rem;
  font-size: var(--fs-eyebrow);
  letter-spacing: var(--track-eyebrow-tight);
  text-transform: uppercase;
  color: var(--muted);
}

.speaking__controls { display: none; }
html.js .speaking__controls { display: flex; align-items: center; gap: 1rem; margin-top: 2.5rem; }
.speaking__arrow {
  width: 2.5rem; height: 2.5rem;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid var(--line-strong);
  border-radius: 9999px;
  color: var(--muted);
  transition: color var(--t-hover) var(--ease-out), border-color var(--t-hover) var(--ease-out);
}
.speaking__arrow:hover { color: var(--accent); border-color: var(--accent); }
.speaking__dots { display: flex; gap: .5rem; margin-left: .5rem; }
.speaking__dot {
  width: .5rem; height: .5rem;
  border-radius: 9999px;
  background: var(--line-strong);
  transition: background var(--t-hover) var(--ease-out);
}
.speaking__dot[aria-pressed="true"] { background: var(--accent); }

.speaking__portrait { aspect-ratio: 3 / 4; }
/* The slot is the photograph's own 3:4, so the frame is the whole picture:
   the files are deliberately uncropped and nothing here crops them. */
.speaking__portrait .liquid__img { object-position: center; }
@media (max-width: 1023px) { .speaking__portrait { max-width: 26rem; } }
