/* SMALL TYPE GROWS WITH THE SCREEN — 2026-08-08.
 *
 * The client's two observations were one fault: "有些字太小了" on a 4K screen,
 * and "保持长宽不变，整体缩小就整体缩小". Measured: this stylesheet had 349
 * vw-clamps and EVERY ONE of them stops growing before 3840 — the design is
 * fluid to about 1400-1900 and frozen after that — while the small labels
 * (eyebrows, dates, roles, filter captions, cookie buttons, review bylines)
 * were flat pixel values that never grew at all. On a 4K screen the big type
 * is frozen and the small type is a third the size it should be, which is why
 * the proportions read as broken.
 *
 * A global `zoom` was tried first and rejected with evidence: at 3840 with
 * zoom 2.29 the layout width stays 3840 and the page overflows sideways by
 * 1600px, because `zoom` does not shrink the layout viewport and every vw in
 * here still resolves against the full width. A vw-driven layout and a global
 * zoom cannot both be true.
 *
 * So the values grow instead. Each small size becomes
 * `clamp(<old>, <old/16.8>vw, <old*1.8>)`: the vw term equals the old value at
 * exactly 1680, so **nothing below 1680 changes by a single pixel** — the
 * laptop rendering the client already approved is untouched — and above it the
 * label scales with the screen to 1.8x, reaching its cap around 3020.
 */
/* MINIMUM TYPE SIZE: 12px — set 2026-08-08 at the client's request.
 *
 * "有些字太小了，可能丧失可读性。对于字的大小方面，这个应该是有明确要求的."
 *
 * The rule is: NO RENDERED TEXT ON THIS SITE IS SMALLER THAN 12px. Measured
 * before it existed, nine public pages carried text at 9, 9.5, 10, 10.5 and 11
 * — 79 declarations in all — and most of it was the letter-spaced small-caps
 * this design leans on: eyebrows, dates, roles, filter labels, the cookie
 * buttons, the review attributions. Small caps at 10px with 0.12em of tracking
 * looks composed on a designer's screen and is genuinely hard to read on a
 * laptop at arm's length, which is where a client reads it.
 *
 * 12px is the floor, not a target: anything already larger is untouched. If a
 * label no longer fits its column at 12px, the column is what changes.
 */
/* ============================================================
   People

   Order of weight on the first screen, settled with the user after
   several passes: the title, then the firm's own sentence, then the
   maxim as a marginal note. The directory below uses the plate card —
   an off-white panel overlapping the foot of each portrait.

   The opening is one colour ground carrying both the hero and the epigraph,
   frosted on the left and sharp on the right. It is built from four static
   images — one sharp, three pre-blurred — rather than a live blur. That reason
   is load-bearing and is set out above `.people-opening__frost`; read it before
   changing anything there, and never regrade one of the four on its own.

   Content is fixed by source parity: 37 people, source order, source
   wording, source filters. Everything here is presentation only. Three
   additions are approved, and there are no others: the maxim, the hero
   label, and the Chinese counterpart of the hero sentence.

   The hero label — "Sydney and nine cities across China" — is a fact read off
   `people-data.js` (37 records; `Sydney, Australia` plus nine distinct Chinese
   cities), but it is hardcoded in the markup. Re-check it whenever the roster
   changes.
   ============================================================ */

.people-page {
  /* One gutter for the hero and the directory so every left edge on the
     page sits on the same line at every viewport.

     The floor is a literal 40px, deliberately NOT `--gutter-min`. That global
     is `clamp(40px, 5.5vw, 120px)` and is tuned for the homepage's sections;
     when it grew, it silently pulled this rail in to 79px at 1440 and took the
     directory with it — four-up cards dropped from 321px to 302px and the
     people audit's `fourUpLayout` check started failing. The directory is a
     portrait grid and wants the full 1460 rail, so it keeps its own floor. */
  --gutter: max(40px, calc((100vw - 1460px) / 2));
  --wall: #f7f5ef;
  --rule: rgba(24, 25, 23, 0.2);
  --rule-strong: rgba(24, 25, 23, 0.52);
  --accent: #7d4634;
  --label: #8a6d3f;
  --grain: url("data:image/svg+xml,%3Csvg viewBox='0 0 180 180' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.45'/%3E%3C/svg%3E");


  /* Standing rule for this route, from the user: no element may be a flat
     untextured white or black. A uniform fill next to a painting reads as
     unfinished, and the directory was two uniform fills — a near-white plate on
     an off-white ground — which is what made it look plain.

     The page's own paper is therefore grained rather than filled. It is a
     viewport-fixed layer, so the grain stays a constant physical size instead of
     scrolling past as a tiling pattern, and it is one cached texture rather than
     a repaint. Everything below the opening — directory and footer — sits on it,
     so the fix is structural rather than applied surface by surface. */
  background-color: var(--wall);
  background-image: var(--grain);
  background-size: 190px 190px;
  background-attachment: fixed;
  /* Blended, not laid on top. `feTurbulence` output is coloured noise, and a
     background image on the page canvas cannot take an `opacity`, so unblended
     it reads as dirt rather than as paper. Soft-light reduces it to a luminance
     texture in the wall colour — the same treatment the field's grain gets. */
  background-blend-mode: soft-light;
}

.people-page .primary-menu > li:first-child > a {
  color: var(--accent);
}

/* ------------------------------------------------------------
   The opening — hero and epigraph on one colour ground

   b57 put a real pigment painting behind the hero and bought legibility with a
   cream wash over its left half. The user's correction at b59: that wash is
   the wrong currency. It buys contrast by *removing colour*, which is why the
   left of the frame kept reading as paper when the whole first screen was meant
   to be in colour.

   The wash is replaced by frosted glass. Contrast now comes from defocusing and
   lifting the ground rather than draining it — `brightness` raises luminance
   while `saturate` holds the hue, so the type reads against colour instead of
   against cream. And because nothing is being covered up, the field can be
   `equilibrium-field`, which carries pigment edge to edge; `luminous-reserve`
   was chosen at b57 precisely because half of it was blank paper, and that is
   now the wrong property to want.

   The ground spans the hero *and* the epigraph, so the first screen is colour
   from the header to somewhere in the directory's approach, then settles to the
   paper the thirty-seven portraits need.

   The rejected canvas field generated from the portraits stays rejected;
   `people-field.js` remains in the repository, unreferenced, as its record.
   ------------------------------------------------------------ */

.people-opening {
  /* One focal point for the sharp field and all three blurred copies. They must
     frame identically or the ramp shows a double image, and a single custom
     property is the only way to guarantee a responsive override cannot move one
     without moving the rest. */
  /* TOP-ALIGNED, BECAUSE THE PAINTING HAS A PERSON IN IT — 2026-08-08.
   *
   * `center` crops equally from both edges, which is right for an abstract
   * field and wrong for this one: equilibrium-field-people.jpg is 2172x869 and
   * carries a figure in profile in its upper right, HER HEAD OCCUPYING 5%-25%
   * OF THE IMAGE HEIGHT. The band is 560px tall whatever the screen, so at
   * 2560 the visible slice was 22.7%-77.3% of the picture and at 3840 it was
   * narrower still — in both, the head sat entirely above the top edge. The
   * client's report was literal: "像没有头一样".
   *
   * Aligning to the top costs the lower mass, which is field rather than
   * subject, and keeps the head with room above it at every width: at 3840
   * the visible slice is now 0%-47%, and she sits between 5% and 25% of it. */
  --field-focus: center top;
  position: relative;
  isolation: isolate;
  overflow: hidden;
}

.people-opening__field,
.people-opening__frost,
.people-opening__settle {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

/* The drift is on the field and on the frost as a whole, never on the
   individual blur layers: one animated transform per stack keeps them in
   register and gives the compositor two moving layers instead of four. */
.people-opening__field,
.people-opening__frost {
  transform: scale(1.04);
  animation: people-field-drift 34s ease-in-out infinite alternate;
  will-change: transform;
}

.people-opening__field {
  z-index: 0;
  background: url("assets/equilibrium-field-people.jpg") var(--field-focus) / cover no-repeat;
}

/* The grain is what separates a painting from a gradient on a screen, and it is
   the same overlay the homepage art carries. */
.people-opening__field::after {
  content: "";
  position: absolute;
  inset: 0;
  opacity: 0.16;
  mix-blend-mode: soft-light;
  background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 180 180' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.45'/%3E%3C/svg%3E");
}

/* ---- Progressive frost ----

   Three stacked copies of the field, each blurrier than the one below it and
   each masked to stop sooner, so focus ramps off toward the left edge. The ramp
   is the point: a single masked blur composites a sharp image against a blurred
   copy of itself, and the join reads as a ghost seam.

   **These are pre-blurred images, not `backdrop-filter`.** An earlier build of
   this used four stacked backdrop filters, and it was measured at 20–30fps
   while scrolling against 115fps with the frost removed — a backdrop filter has
   to re-sample and re-blur whatever is behind it on every frame the layer
   moves, and this one moves for the whole length of the page. Sizing the layers
   to their masks did not help; the cost is per layer, not per area, and even a
   single layer only reached about 45fps. The shipped version measures 120fps.

   Blurring a *static* image is work that can be done once, so it is done ahead
   of time. Each level ships as a small JPEG — a blurred image holds no
   high-frequency detail, so 170–520px wide is all the information there is, and
   the browser's upscaling supplies the rest of the smoothing. All three
   together are 11KB. The saturation and luminance lift that carries dark type
   is baked into them for the same reason.

   Registration matters twice over: every layer uses the same `cover` framing and
   the same drift transform as the sharp field, *and* every layer is graded from
   the same source in the same pass. A build where the sharp layer was regraded
   and the blurred copies were not put a visible tonal step in the middle of the
   ramp. Regenerate all four together or not at all. */

.people-opening__frost {
  z-index: 1;
}

.people-opening__frost span {
  position: absolute;
  inset: 0;
  display: block;
  background-position: var(--field-focus);
  background-repeat: no-repeat;
  background-size: cover;
}

.people-opening__frost span:nth-child(1) {
  background-image: url("assets/equilibrium-field-people-soft.jpg");
  mask-image: linear-gradient(90deg, #000 0%, #000 40%, transparent 70%);
  -webkit-mask-image: linear-gradient(90deg, #000 0%, #000 40%, transparent 70%);
}

.people-opening__frost span:nth-child(2) {
  background-image: url("assets/equilibrium-field-people-softer.jpg");
  mask-image: linear-gradient(90deg, #000 0%, #000 26%, transparent 54%);
  -webkit-mask-image: linear-gradient(90deg, #000 0%, #000 26%, transparent 54%);
}

.people-opening__frost span:nth-child(3) {
  background-image: url("assets/equilibrium-field-people-softest.jpg");
  mask-image: linear-gradient(90deg, #000 0%, #000 14%, transparent 40%);
  -webkit-mask-image: linear-gradient(90deg, #000 0%, #000 14%, transparent 40%);
}

/* A thin scrim, not a wash. The frost defocuses and lifts the ground but does
   not flatten its darks, and this only has to close the last of the gap.

   Weighted to the gutter edge: the field's darkest mass sits under the start of
   every line, and after the ground was deepened for gravitas that edge was where
   the label and the Chinese line fell below AA. */
.people-opening__frost::after {
  content: "";
  position: absolute;
  inset: 0;
  background:
    /* A light coat under the masthead only, fading out well before the title.
       The navigation is dark ink on whatever the painting happens to put behind it,
       and the header's own frost is at its thinnest at the top of the page — it
       develops with scroll, so at rest there is almost nothing under the nav. On the
       previous painting the worst link measured 2.5:1; on this one, whose upper right
       is deep violet, the active link fell to 1.3:1. This lifts that band without
       touching the shared header, which another pass is currently tuning, and without
       reaching far enough down to wash out the artwork. */
    linear-gradient(
      180deg,
      rgba(250, 247, 240, var(--masthead-coat, 0.55)) 0,
      rgba(250, 247, 240, calc(var(--masthead-coat, 0.55) * 0.62)) var(--header-height),
      rgba(250, 247, 240, 0) calc(var(--header-height) + 56px)
    ),
    linear-gradient(
      90deg,
      rgba(250, 247, 240, 0.4) 0%,
      rgba(250, 247, 240, 0.26) 22%,
      rgba(250, 247, 240, 0.09) 50%,
      rgba(250, 247, 240, 0) 70%
    );
}

/* Where the colour hands over to the paper the directory is set on. Kept inside
   the opening so the handover happens under the epigraph rather than at a
   section boundary, which would read as two stacked bands again. */
.people-opening__settle {
  z-index: 2;
  background: linear-gradient(180deg, rgba(247, 245, 239, 0) 62%, rgba(247, 245, 239, 0.72) 84%, var(--wall) 100%);
}

/* Masks are what confine the frost to the left. Without support the blurred
   copies would cover the whole field, hiding the sharp half entirely — legible,
   but the design would be gone — so the unmasked path drops the blur layers and
   falls back to the earlier cream wash rather than letting them run full width. */
@supports not ((mask-image: linear-gradient(#000, transparent)) or (-webkit-mask-image: linear-gradient(#000, transparent))) {
  .people-opening__frost span {
    display: none;
  }

  .people-opening__frost::after {
    background:
      linear-gradient(
        90deg,
        rgba(249, 246, 239, 0.93) 0%,
        rgba(249, 246, 239, 0.82) 24%,
        rgba(249, 246, 239, 0.5) 44%,
        rgba(249, 246, 239, 0.1) 64%,
        rgba(26, 18, 28, 0.09) 100%
      );
  }
}

.people-hero {
  /* 520px at 1440, the height the user chose: tall enough for the painting to
     read as a painting rather than a colour band, short enough that the
     directory still breaks the fold. Tracks viewport width, not height — the
     display type is set in vw, so a vh-driven hero strands a dead band on tall
     narrow screens. */
  /* The cap was 560px and it is the same shape of fault as the homepage's
     880px reading band: 36vw is the intent — the band tracks the width so the
     painting keeps its proportions — and the cap silently ends that at 1556px.
     Past there the box only got wider, so `cover` cropped harder and harder
     (464px of the picture at 2560, 976px at 3840). 720 keeps the growth going
     to 2000px and leaves the directory breaking the fold at every size. */
  min-height: clamp(430px, 36vw, 720px);
  position: relative;
  display: grid;
  align-content: center;
  padding: calc(var(--header-height) + clamp(46px, 4.4vw, 70px)) 0 clamp(44px, 3.22917vw, 62px);
  color: var(--ink);
  @media (max-width: 1180px) {
    padding: calc(var(--header-height) + clamp(46px, 4.4vw, 70px)) 0 clamp(44px, 4vw, 62px);
  }
}

/* The first screen holds one proposition. Every earlier arrangement also
   carried the maxim here, and the user identified why none of them composed —
   two prose blocks of the same rhetorical register were both trying to be *the*
   statement, so they cancelled each other out and no layout could rescue it.
   The maxim has its own band below. What the stack gained instead is
   counterpoint: a rust label, the title, the sentence, and its Chinese
   counterpart — four sizes, three colours, one left edge.

   The measure is deliberately not capped here but on the two prose lines
   individually, so the label's rule can run gutter to gutter off this
   container's own width. A `100vw` stretch would overshoot the right gutter by
   the scrollbar on every platform that still reserves one. */
.people-hero__content {
  position: relative;
  z-index: 3;
  margin: 0 var(--gutter);
}

/* The old hairline ran the full width to nothing — a line with no errand. It
   now carries the label: the rule starts where the words end and runs out to
   the right margin, which both gives it a job and walks the eye off the type
   and into the pigment. That crossing is the whole repair to "the colour and
   the type never meet". */
.people-hero__eyebrow {
  margin: 0 0 clamp(22px, 2.4vw, 34px);
  display: flex;
  align-items: center;
  gap: clamp(14px, 1.35417vw, 26px);
  /* Deeper than the page's `--accent`, and hero-scoped. Deepening the ground for
     gravitas cost the label contrast — at 10.5px it is small text and needs
     4.5:1, and the page rust only reached 3:1 against the graded field. The
     lighter rust stays correct everywhere it sits on paper. */
  color: #52281b;
  font-size: clamp(10.5px, 0.625vw, 18.9px);
  font-weight: 700;
  letter-spacing: 0.26em;
  line-height: 1.15;
  text-transform: uppercase;
  @media (max-width: 1180px) {
    gap: clamp(14px, 1.6vw, 26px);
  }
}

.people-hero__eyebrow::after {
  content: "";
  flex: 1;
  height: 1px;
  background: linear-gradient(90deg, rgba(125, 70, 52, 0.42), rgba(125, 70, 52, 0.1) 62%, rgba(125, 70, 52, 0));
}

/* Set at −0.022em, not the −0.075em of the first build. Crushing an old-style
   serif's side bearings at display size makes the letterforms collide, which is
   what read as "ugly"; the size was never the problem. */
.people-hero h1 {
  margin: 0 0 0 -0.04em;
  color: #1c1a17;
  font-family: var(--serif);
  font-size: clamp(58px, 5.625vw, 108px);
  font-weight: 400;
  letter-spacing: -0.022em;
  line-height: 0.84;
  @media (max-width: 1180px) {
    font-size: clamp(58px, 7.2vw, 108px);
  }
}

/* The gap is measured against the descender, not the box: the title sets at
   line-height 0.84, so the "p" of "People" reaches well below its block and a
   nominal gap reads far tighter than its number suggests. */
.people-hero__intro {
  max-width: min(100%, 660px);
  margin: clamp(19px, 2.1vw, 32px) 0 0;
  color: #34312e;
  font-family: var(--serif);
  font-size: clamp(21px, 1.66667vw, 32px);
  letter-spacing: -0.026em;
  line-height: 1.15;
  text-wrap: pretty;
  @media (max-width: 1180px) {
    font-size: clamp(21px, 1.95vw, 32px);
  }
}

/* The Chinese counterpart of the firm's own sentence, in the homepage's
   register for the same device: quieter, tracked open, never a second claim.

   Its quietness comes from size and tracking rather than from being the palest
   thing on the page. At 17px it is small text, and pale grey on the graded field
   measured 2.7:1. */
.people-hero__zh {
  max-width: min(100%, 620px);
  margin: clamp(13px, 1.3vw, 19px) 0 0;
  color: #3d3730;
  font-family: var(--serif);
  font-size: clamp(14px, 1.12vw, 17px);
  font-weight: 400;
  letter-spacing: 0.055em;
  line-height: 1.15;
}

/* The route hero uses the shared quiet-settle system. Its typography is fully
   composed at first paint; no route-specific blur, scale or travel is layered
   over it. */


/* A very slow breath on the painting. Held to about 1.5% of travel: the point
   is that the field is never quite still, not that anything is seen to move. */
@keyframes people-field-drift {
  from { transform: translate3d(-0.9%, -0.5%, 0) scale(1.04); }
  to { transform: translate3d(0.9%, 0.5%, 0) scale(1.075); }
}

/* ------------------------------------------------------------
   Directory
   ------------------------------------------------------------ */

/* No opaque fill: the page's grained paper shows through. The directory adds
   only the residue of the painting above it — three very large, very faint
   masses in the opening's own graded palette, anchored to the top edge so they
   take over exactly where the settle hands off, then thin out over the first
   couple of thousand pixels. Sized in px rather than percentages because the
   directory is roughly nine thousand pixels tall and percentage radii would
   scale the wash to the whole roll. */
.people-directory {
  /* THIRTY PER CENT OFF, at the client's direction: the roster sat in more space
     than it needed at both ends and the whole block reads better higher up the
     page. Every length here is the previous one times 0.7, floors and ceilings
     included, so the ramp keeps its shape and the section shortens by the same
     proportion at every width rather than collapsing on small screens where the
     floor was doing the work. Measured at 1440: 56px above became 40px and 110px
     below became 78px. */
  padding: clamp(35px, 2.77083vw, 53px) var(--gutter) clamp(67px, 5.39583vw, 104px);
  position: relative;
  isolation: isolate;
  background: transparent;
  @media (max-width: 1180px) {
    padding: clamp(35px, 3.78vw, 53px) var(--gutter) clamp(67px, 7vw, 104px);
  }
}

.people-directory::before {
  content: "";
  position: absolute;
  z-index: -1;
  inset: 0;
  pointer-events: none;
  background:
    radial-gradient(1500px 820px at 6% -6%, rgba(118, 92, 110, 0.15), transparent 70%),
    radial-gradient(1250px 660px at 94% 1%, rgba(150, 118, 80, 0.13), transparent 68%),
    radial-gradient(1000px 560px at 44% 1500px, rgba(90, 96, 120, 0.07), transparent 70%);
  /* Two of the three ellipses are centred on or above this box's top edge, so
     unmasked they are already at strength on row 0 and butt the opening's
     `__settle`, which has by then resolved to flat `--wall`. Measured as a wedge
     — nothing at the left gutter widening to 15/255 by x=1260, which is why it
     read as an edge across the right two-thirds rather than as a gradient. Both
     ends are feathered: with the grid filtered down to nothing the section is
     short, and the bottom edge then butts the footer the same way. */
  mask-image: linear-gradient(180deg, transparent 0, #000 220px, #000 calc(100% - 160px), transparent 100%);
  -webkit-mask-image: linear-gradient(180deg, transparent 0, #000 220px, #000 calc(100% - 160px), transparent 100%);
}

/* The footer is styled in the shared sheet and filled with `--paper`. Scoped to
   this route so it stops covering the grained paper; the other two routes keep
   the shared fill. */
/* ONLY THE OLD FOOTER. This clears the shared paper fill so the roster's grained
   ground runs under it — right for a footer that was paper, and catastrophic for
   the one that replaced it: the current footer is paper-coloured type on ink, and
   removing its ground left every word of it invisible on this route alone. Found
   by the client, on the one page that carries this override. */
.people-page .site-footer:not(:has(.footer-inner)) {
  background: transparent;
}

.people-filters {
  min-height: 66px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: clamp(15px, 1.25vw, 24px);
  border-top: 1px solid var(--rule-strong);
  border-bottom: 1px solid rgba(24, 25, 23, 0.12);
  @media (max-width: 1180px) {
    gap: 24px;
  }
}

.people-filters__intro {
  min-width: 96px;
  color: var(--label);
  font-size: clamp(10px, 0.5952vw, 18px);
  font-weight: 700;
  letter-spacing: 0.15em;
  text-transform: uppercase;
}

.people-filters__controls {
  flex: 1;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-end;
  gap: 0 clamp(16px, 1.8vw, 30px);
}

.people-filter-label {
  margin-right: clamp(4px, 0.8vw, 14px);
  color: #6d6a63;
  font-size: clamp(10px, 0.5952vw, 18px);
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

/* Filters read as an editorial rail, not as buttons in boxes: the state is
   carried by weight, colour and a single hairline. */
.people-filter {
  min-height: 40px;
  padding: 0 1px;
  position: relative;
  border: 0;
  border-bottom: 1px solid transparent;
  color: #57544d;
  background: none;
  cursor: pointer;
  font: inherit;
  font-size: 12.5px;
  letter-spacing: 0.012em;
  transition: color 320ms ease, border-color 320ms ease;
}

.people-filter:hover {
  color: var(--ink);
  border-bottom-color: rgba(24, 25, 23, 0.32);
}

.people-filter.is-selected {
  color: var(--accent);
  border-bottom-color: var(--accent);
}

.people-clear {
  min-height: 40px;
  margin-left: clamp(6px, 1vw, 16px);
  padding: 0 1px;
  border: 0;
  color: var(--label);
  background: none;
  cursor: pointer;
  font: inherit;
  font-size: clamp(11px, 0.6548vw, 19.8px);
  letter-spacing: 0.03em;
  text-decoration: underline;
  text-decoration-color: rgba(138, 109, 63, 0.4);
  text-underline-offset: 5px;
  transition: color 300ms ease, transform 300ms ease;
}

.people-clear:hover {
  color: #5c3328;
  transform: translateX(-2px);
}

/* Kept in the layout when inactive rather than removed from it. The row is
   `justify-content: flex-end`, so un-hiding this button on the first filter
   click used to drag every control 88px to the left — the whole rail jumped
   underneath the pointer that had just landed on it. Hidden this way it is still
   out of the accessibility tree and unfocusable, but it no longer moves anything
   when it appears. */
.people-clear[hidden] {
  display: inline-block;
  visibility: hidden;
  pointer-events: none;
}

.people-filter:focus-visible,
.people-clear:focus-visible,
.person-card a:focus-visible {
  outline: 2px solid #a9844f;
  outline-offset: 5px;
}

/* ------------------------------------------------------------
   Directory cards

   The floating information plate is restored at the user's request: the
   off-white card overlaps the foot of the portrait, which gives the grid a
   layered construction the flat "wall label" treatment did not have. The
   interim wall-label build, its two-datum hang and the portrait duotone are
   all withdrawn.
   ------------------------------------------------------------ */

.people-grid {
  width: 100%;
  /* THE PAGE'S OWN SPINE, 1460, not a hundred pixels inside it. The cap used to
     be 1360, and `--gutter` on this route is `max(40px, (100vw - 1460px) / 2)`,
     so above about 1540px the roster was inset 50px further on each side than
     the filter row directly above it and the footer directly below it — at 2560
     the cards started at 593 while everything else on the page started at 543.
     Home's own card grid runs the full 1460. Below 1540 nothing moves: the
     gutter is already the binding constraint there. */
  max-width: 1460px;
  /* The same 0.7. This is the gap between the filter row and the first card, and
     leaving it while the ends came in would have made the filters look detached
     from the grid they filter. The gap BETWEEN cards is untouched: that is the
     grid's own rhythm, not the space around it. */
  margin: clamp(22px, 1.45833vw, 28px) auto 0;
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: clamp(21px, 1.77083vw, 34px) 24px;
  @media (max-width: 1180px) {
    margin: clamp(22px, 2.38vw, 28px) auto 0;
    gap: 34px 24px;
  }
}

.person-card {
  width: 100%;
  min-width: 0;
  justify-self: center;
  opacity: 1;
  transform: none;
}

.person-card[hidden] {
  display: none;
}

.person-card > a {
  height: 100%;
  position: relative;
  display: flex;
  flex-direction: column;
  color: var(--ink);
  text-decoration: none;
}

.person-card__portrait {
  aspect-ratio: 0.91;
  position: relative;
  overflow: hidden;
  background:
    radial-gradient(circle at 73% 16%, rgba(187, 140, 94, 0.42), transparent 27%),
    radial-gradient(circle at 22% 86%, rgba(116, 125, 159, 0.36), transparent 34%),
    linear-gradient(135deg, #e8e5dd, #cfc8c4);
}

.person-card:nth-child(3n + 2) .person-card__portrait {
  background:
    radial-gradient(circle at 18% 16%, rgba(154, 122, 161, 0.36), transparent 30%),
    radial-gradient(circle at 82% 90%, rgba(208, 120, 75, 0.34), transparent 36%),
    linear-gradient(135deg, #ebe8e2, #d0c9c5);
}

.person-card:nth-child(3n) .person-card__portrait {
  background:
    radial-gradient(circle at 82% 24%, rgba(196, 147, 72, 0.38), transparent 30%),
    radial-gradient(circle at 28% 82%, rgba(92, 108, 145, 0.37), transparent 34%),
    linear-gradient(135deg, #e8e4dc, #c9c5c3);
}

.person-card__portrait::after {
  content: "";
  position: absolute;
  z-index: 1;
  inset: 0;
  background: linear-gradient(180deg, transparent 56%, rgba(26, 24, 25, 0.2));
  transition: opacity 450ms ease;
}

/* Skin tone is left alone. An earlier build replaced every portrait's hue with
   a warm-to-violet ramp to unify the two photo sources; it did unify them, and
   it tinted every face doing so. */
.person-card__portrait img {
  width: 100%;
  height: 100%;
  position: relative;
  z-index: 0;
  display: block;
  object-fit: cover;
  object-position: center top;
  /* NO RESTING FILTER. This carried `saturate(0.84) contrast(0.97)`, which is
     the last remnant of the warm-to-violet ramp the note above records being
     rejected — quieter than that ramp, and still a treatment applied to every
     face on the page. Measured against the same render with the filter off, it
     removed 12.9% of the colour from the roster, and the hover state put nearly
     all of it back (`saturate(0.98)`), so a photograph came alive when the mouse
     touched it and went grey again when it left. The client's word was 灰灰的.
     The photographs now show at their own colour, at rest and on hover alike,
     and the hover is the scale below and nothing else. */
  transform: scale(1.001);
  transition: transform 850ms cubic-bezier(0.22, 1, 0.36, 1);
}

/* The plate: frosted glass.

   Six treatments were rejected before this — a flat fill, a gradient with a
   tick, parchment with a gold monogram, gold pinstripe, gold weave, and a
   tinted glass simulation. The fault common to all of them was that each
   introduced a material this site does not have. Frosted glass is the one
   material it *does* have: the whole opening is frosted glass over pigment.

   **It is a real `backdrop-filter`, and that is a measured choice.** The hero's
   frost could not use one — a full-width 1440x700 layer that scrolls the length
   of the page measured 20-30fps. These panels are about 350x106, so the total
   blurred area is roughly half the hero's and it costs nothing: 37 cards with
   live blur measured 119-120fps against 119-120 without. Live blur is also the
   better answer here, because the glass then responds to the actual photograph
   instead of to a stand-in.

   The overlap is 61px for a reason: with materially less overlap there is almost
   nothing behind the glass to see, and glass with nothing behind it is just a
   grey panel. It now straddles the foot of the portrait, so the top of the panel
   carries the blurred photograph, the bottom carries the paper, and the edge of
   the image itself reads softly through the frost.

   What makes it read as glass rather than as translucency, in order of how much
   each one matters:
     1. the rim light along the top edge — the single strongest tell;
     2. saturation lifted well above 1, because real frosted glass intensifies
        the colour it diffuses rather than greying it;
     3. a shadow cast *upward* onto the photograph, so the pane sits proud of it;
     4. surface frost, which is a physical texture and not a fill;
     5. a raking specular sheen across the pane. */
.person-card__meta {
  min-height: clamp(65px, 5.52083vw, 106px);
  /* Fills the height the portrait leaves, so every plate in a row ends on the
     same line. Without it the plate was only as tall as its own text and a
     2-line role next to a 6-line one left the row's plate bottoms up to 64px
     apart, with the gold location rules — the card's second register — 16px out
     of true with each other. `margin-top: auto` on the location then pins that
     rule to the plate's foot, so the registers line up across the row. */
  flex: 1 1 auto;
  position: relative;
  z-index: 2;
  overflow: hidden;
  margin: -61px 13px 0;
  padding: 15px 16px 16px;
  display: flex;
  flex-direction: column;
  border: 1px solid rgba(255, 255, 255, 0.4);
  border-top-color: rgba(255, 255, 255, 0.7);
  background: linear-gradient(
    180deg,
    rgba(252, 252, 254, 0.74) 0%,
    rgba(246, 247, 251, 0.67) 52%,
    rgba(241, 242, 247, 0.56) 100%
  );
  backdrop-filter: blur(20px) saturate(1.8) brightness(1.04);
  -webkit-backdrop-filter: blur(20px) saturate(1.8) brightness(1.04);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.75),
    inset 0 -1px 0 rgba(255, 255, 255, 0.22),
    0 -7px 20px rgba(14, 14, 20, 0.1),
    0 16px 34px rgba(14, 14, 20, 0.1);
  transition:
    transform 400ms cubic-bezier(0.22, 1, 0.36, 1),
    box-shadow 400ms ease,
    backdrop-filter 400ms ease;
  @media (max-width: 1180px) {
    min-height: 106px;
  }
}

/* Raking sheen. Sits above the pane and below the type. */
.person-card__meta::before {
  content: "";
  position: absolute;
  z-index: -1;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(
    122deg,
    rgba(255, 255, 255, 0.32) 0%,
    rgba(255, 255, 255, 0.05) 30%,
    rgba(255, 255, 255, 0) 58%,
    rgba(255, 255, 255, 0.13) 100%
  );
}

/* The frosting itself. Without it the pane is optically perfect, which is the
   one thing frosted glass is not. */
.person-card__meta::after {
  content: "";
  position: absolute;
  z-index: -1;
  inset: 0;
  pointer-events: none;
  opacity: 0.38;
  mix-blend-mode: soft-light;
  background-image: var(--grain);
  background-size: 130px 130px;
}

.person-card__meta h2 {
  max-width: calc(100% - 25px);
  margin: 0;
  font-family: var(--serif);
  font-size: clamp(21px, 1.45833vw, 28px);
  font-weight: 400;
  letter-spacing: -0.04em;
  /* 0.98 is right for the one-line names and collides on the two-line ones: at
     display size it put the descender of `Jiaying`/`Zhimeng`/`Yunyi` into the caps and
     parenthesis of the line below on 8 of the 37 cards, across the whole
     521-1180px band and again at 390. Display leading has to clear a descender
     the moment a second line is possible. */
  line-height: 1.06;
  /* Splits `Jiachen Fan (Carrie)` as `Jiachen` / `Fan (Carrie)` rather than
     orphaning the bracket — a line starting on `(` loses the card's left edge to
     the glyph's side bearing, which is the alignment the whole grid is built on.
     Paired with the non-breaking space `people.js` binds before the bracket. */
  text-wrap: balance;
  @media (max-width: 1180px) {
    font-size: clamp(21px, 1.65vw, 28px);
  }
}

.person-card__role,
.person-card__location {
  margin: 8px 0 0;
  /* Deepened for the glass. The role line sits in the band where the blurred
     photograph is behind the pane, and every second portrait is a dark suit —
     at #5b5953 it measured 2.65-4.21:1 across twelve cards. The name survives
     because its ink is far darker; the location survives because the paper, not
     the photograph, is behind it. Hierarchy here is carried by size, 10px
     against 22–28px, which is a wide enough gap to lose the tonal step. */
  color: #333230;
  font-size: clamp(10px, 0.5952vw, 18px);
  letter-spacing: 0.025em;
  line-height: 1.15;
}

/* The gap between the role and the location used to be dead space with a line
   floating at the bottom of it. A rule turns it into the card's second register:
   who above, where below.

   The gold is deeper than the page's `--label`. At 9px bold this is small text
   and needs 4.5:1; the old gold measured 4.2:1 on the flat plate and would have
   dropped to 3.8:1 on the graded one, so it was already short before this
   change and is now fixed rather than inherited. */
/* The hero's device, repeated at card scale: the label, then a hairline running
   from where its words end out to the edge. It is the one mark on this page that
   touches two things at once, and it is why the card reads as part of the page
   rather than as a component dropped onto it. */
.person-card__location {
  margin-top: auto;
  padding: 12px 0 0;
  display: flex;
  align-items: center;
  gap: 10px;
  color: #6f5529;
  font-size: clamp(9px, 0.5357vw, 16.2px);
  font-weight: 700;
  letter-spacing: 0.12em;
  line-height: 1.15;
  text-transform: uppercase;
}

.person-card__location::after {
  content: "";
  flex: 1;
  height: 1px;
  background: linear-gradient(90deg, rgba(74, 60, 44, 0.3), rgba(74, 60, 44, 0.06));
}

.person-card__arrow {
  width: 14px;
  height: 14px;
  position: absolute;
  top: 18px;
  right: 16px;
  fill: none;
  stroke: #8d7044;
  stroke-linecap: square;
  stroke-linejoin: miter;
  stroke-width: 1.6;
  transition: transform 360ms cubic-bezier(0.22, 1, 0.36, 1);
}

.person-card:hover .person-card__portrait::after {
  opacity: 0.38;
}

.person-card:hover .person-card__portrait img {
  transform: scale(1.028);
}

/* The leaf lifts and the foil catches more light. Nothing touches the
   background layers — hovering must not reset the card to a flat fill, which is
   what the old `background: #fcfaf5` did to every layer at once. */
/* The pane lifts and the frost clears a little — glass reacting to being
   picked up, rather than a card changing colour. */
.person-card:hover .person-card__meta,
.person-card:focus-within .person-card__meta {
  backdrop-filter: blur(26px) saturate(1.95) brightness(1.07);
  -webkit-backdrop-filter: blur(26px) saturate(1.95) brightness(1.07);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.85),
    inset 0 -1px 0 rgba(255, 255, 255, 0.26),
    0 -9px 24px rgba(14, 14, 20, 0.13),
    0 24px 44px rgba(14, 14, 20, 0.14);
  transform: translateY(-3px);
}

.person-card:hover .person-card__meta::before,
.person-card:focus-within .person-card__meta::before {
  opacity: 1;
}

.person-card:hover .person-card__arrow,
.person-card:focus-within .person-card__arrow {
  transform: translate(4px, -4px);
}

.people-empty {
  margin: clamp(32px, 2.70833vw, 52px) 0 0;
  color: #625d55;
  font-family: var(--serif);
  font-size: clamp(17px, 1.45833vw, 28px);
  @media (max-width: 1180px) {
    margin: 52px 0 0;
    font-size: 28px;
  }
}

.people-empty[hidden] {
  display: none;
}

/* ------------------------------------------------------------
   Responsive
   ------------------------------------------------------------ */

@media (max-width: 1180px) {
  .people-page {
    --gutter: 28px;
  }

  /* Four-up belongs to the wide editorial frame. On a smaller desktop the
     directory returns to three columns and preserves the deliberately reduced
     card footprint from b112, instead of squeezing four unreadable plates into
     the row. */
  .people-grid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }

  .person-card {
    width: 80%;
  }
}

@media (max-width: 960px) {
  .people-hero {
    min-height: 470px;
    padding-top: calc(var(--header-height) + 78px);
  }

  /* The copy takes a larger share of a narrower frame, so the clear half has to
     give ground: every mask stop moves right by roughly a third of the distance
     it had. The ramp survives — it just starts later. */
  .people-opening__frost span:nth-child(1) {
    mask-image: linear-gradient(90deg, #000 0%, #000 58%, transparent 86%);
    -webkit-mask-image: linear-gradient(90deg, #000 0%, #000 58%, transparent 86%);
  }

  .people-opening__frost span:nth-child(2) {
    mask-image: linear-gradient(90deg, #000 0%, #000 42%, transparent 72%);
    -webkit-mask-image: linear-gradient(90deg, #000 0%, #000 42%, transparent 72%);
  }

  .people-opening__frost span:nth-child(3) {
    mask-image: linear-gradient(90deg, #000 0%, #000 28%, transparent 58%);
    -webkit-mask-image: linear-gradient(90deg, #000 0%, #000 28%, transparent 58%);
  }

  .people-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .people-hero__intro {
    max-width: min(100%, 620px);
  }
}

@media (max-width: 680px) {
  .people-page {
    --gutter: 20px;
  }

  .people-hero {
    min-height: 420px;
    padding-top: calc(var(--header-height) + 50px);
    padding-bottom: 42px;
  }

  /* A left-frosted / right-clear ramp is a wide-frame idea and it does not
     survive a phone: the copy runs the full measure, so a clear right side would
     be clear underneath text. The whole opening is frosted here instead, which
     costs nothing that matters — frosting defocuses colour, it does not remove
     it, so a phone still gets a colour first screen.

     Collapsing to a single layer is also the performance answer. Four stacked
     backdrop filters on a phone GPU is the one part of this construction that
     could actually cost frames, and with no ramp to build there is nothing for
     the other three to do. */
  .people-opening__frost span:nth-child(1),
  .people-opening__frost span:nth-child(3) {
    display: none;
  }

  .people-opening__frost span:nth-child(2) {
    mask-image: none;
    -webkit-mask-image: none;
  }

  /* A phone sees about a fifth of the artwork's width, and centred that fifth
     is its cream middle — which is how a colour-first build ends up rendering a
     beige screen on the device most people will open it on. The window moves
     onto the warm mass at the right instead. */
  .people-opening {
    --field-focus: 86% center;
  }

  .people-opening__frost::after {
    background: linear-gradient(180deg, rgba(250, 247, 240, 0.4), rgba(250, 247, 240, 0.26) 62%, rgba(250, 247, 240, 0.12));
  }

  .people-hero__eyebrow {
    margin-bottom: 18px;
    font-size: var(--phone-type-label);
    letter-spacing: 0.14em;
  }

  .people-hero h1 {
    margin-left: -0.025em;
    font-size: var(--phone-type-display);
    line-height: 0.92;
  }

  .people-hero__intro {
    max-width: 100%;
    margin-top: 18px;
    font-size: var(--phone-type-subheading);
    line-height: 1.2;
  }

  .people-hero__zh {
    max-width: 100%;
    margin-top: 12px;
    font-size: 14px;
    letter-spacing: 0.04em;
    line-height: 1.3;
  }

  .people-directory {
    /* 36 -> 25, the same 0.7 as the wider rules. A fixed number here is what
       kept the phone at its old spacing while every other width came in. */
    padding-top: 25px;
  }

  .people-filters {
    min-height: 0;
    display: block;
    padding: 18px 0 19px;
  }

  .people-filters__controls {
    justify-content: flex-start;
    margin-top: 12px;
    gap: 0 clamp(14px, 4.4vw, 22px);
  }

  .people-filter-label {
    width: 100%;
    margin: 0 0 2px;
  }

  .people-filters__intro,
  .people-filter-label {
    font-size: var(--phone-type-label);
  }

  .people-filter,
  .people-clear {
    min-height: 42px;
    font-size: 14px;
  }

  .person-card__role {
    margin-top: 7px;
    font-size: 13px;
    line-height: 1.25;
  }

  .person-card__location {
    padding-top: 9px;
    font-size: var(--phone-type-label);
    letter-spacing: 0.09em;
    line-height: 1.2;
  }

  .person-card__location::after {
    min-width: 24px;
  }

  .people-clear {
    margin-left: 0;
  }

  .people-grid {
    margin-top: 20px;
  }
}

/* The single column starts at 520px rather than 680px. The overlapping plate
   needs real width to read as a plate, so it cannot go to two columns on a
   phone — but between 520 and 680 a two-up card is still about 310px wide,
   which is the same width the plate has in the three-column desktop grid at
   1024px. Left at one column across that whole band the directory reached
   29,905px at 680px, which is not a page anyone finishes. */
@media (max-width: 520px) {
  .people-grid {
    grid-template-columns: 1fr;
    gap: clamp(20px, 1.66667vw, 32px);
    @media (max-width: 1180px) {
      gap: 32px;
    }
}

  .person-card__portrait {
    aspect-ratio: 1.04;
  }

  .person-card__meta {
    min-height: clamp(69px, 5.83333vw, 112px);
    /* The mobile values preserve the desktop card's 80% scale: the pane still
       straddles the portrait edge, rather than becoming an opaque label hanging
       almost entirely over bare paper. */
    margin: -54px 10px 0;
    padding: 14px 14px 15px;
    @media (max-width: 1180px) {
      min-height: 112px;
    }
}

  .person-card__meta h2 {
    font-size: clamp(21px, 5.64vw, 22px);
    line-height: 1.08;
    @media (max-width: 1180px) {
      font-size: clamp(21px, 5.64vw, 22px);
    }
}

  .person-card__arrow {
    top: 16px;
    right: 14px;
  }
}

@media (prefers-reduced-motion: reduce) {
  /* Both stacks stop, and both keep the scale — dropping it on one only would
     unregister the blur layers from the field they are blurring. */
  .people-opening__field,
  .people-opening__frost {
    animation: none;
    transform: scale(1.04);
  }

  .person-card,
  .person-card__portrait img,
  .person-card__portrait::after,
  .person-card__meta,
  .person-card__arrow {
    animation: none;
    transition: none;
  }

  .person-card {
    opacity: 1;
    transform: none;
  }
}
