/* 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.
 */
/* ============================================================
   Contact — b56

   A quiet, architectural conversion page:
   1. direct introduction and actions;
   2. compact client evidence;
   3. one coherent Sydney consultation desk.

   The spatial globe is inline SVG and has no animation loop. Generic
   component development is owned by the shared quiet-settle system.
   ============================================================ */

.contact-page {
  --contact-gutter: max(var(--gutter-min), calc((100vw - 1480px) / 2));
  /* HOW TALL THE REVIEW BAND IS — declared here, on the page, because TWO
   * elements need it and they are siblings.
   *
   * The band is lifted over the hero by exactly its own height, so the section
   * after it still begins one screen down (see .testimonials-section). That
   * was written as a property of the band alone, and the hero knew nothing
   * about it — which is the whole of the defect the client photographed on an
   * iPad: Safari's tab bar and address bar take 140-180px off the screen, the
   * hero's content is pushed DOWN by a fraction of what is left, the band is
   * pulled UP by a fixed number, and below about 700px of window the two meet.
   * Measured on the deployed page: at 1194x694 the reviews heading was drawn
   * 15px ON TOP of the enquiry button, at 1194x664 by 42px, at 1024x598 by 112.
   *
   * With the height on the page, .contact-hero can reserve it as bottom padding
   * and the overlap becomes arithmetically impossible rather than unlikely. */
  --contact-reviews-h: calc(
    clamp(40px, 3.6vw, 56px) +
    clamp(56px, 5vw, 76px) +
    clamp(195px, calc(154px + 4.2vw), 204px)
  );
  @media (min-width: 961px) {
    --contact-reviews-h: calc(
      clamp(40px, 2.9vw, 56px) +
      clamp(80px, 5.7vw, 104px) +
      clamp(228px, calc(165px + 5.32vw), 251px)
    );
  }
  @media (min-width: 961px) and (max-width: 1180px) {
    --contact-reviews-h: calc(
      clamp(40px, 3.6vw, 56px) +
      clamp(80px, 5.7vw, 104px) +
      clamp(219px, calc(178px + 4.2vw), 228px)
    );
  }
  --contact-paper: #f4f1e9;
  --contact-paper-deep: #e8e3d9;
  --contact-ink: #1d1d1a;
  --contact-muted: #68645e;
  --contact-line: rgba(29, 29, 26, 0.2);
  --contact-copper: #8d4f38;
  --contact-gold: #b48d54;
  --contact-plum: #625569;
  --contact-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");
  background: var(--contact-paper);
}

/* A shade under `--contact-copper`, and only here. Every other nav item is
   near-black and has margin to spare; the current-page item is the one coloured
   label in the masthead, so it is the only one the artwork can threaten. The
   page copper stays correct everywhere it sits on paper. */
.contact-page .primary-menu > li:last-child > a {
  color: #874a34;
}

/* ------------------------------------------------------------
   Hero
   ------------------------------------------------------------ */

/* THE FIRST SCREEN IS THE SCREEN — 2026-08-08.
 *
 * This was `min-height: clamp(392px, 44vh, 508px)`, which is 49% of a 1440x900
 * window and 38% of a 2560x1400 one. The artwork behind it is `inset: 0` on
 * this element, so it was never "only behind the title" in the CSS — the
 * SECTION was short, and the painting was short with it. Raising the section
 * is the whole fix; the field, the frost and the settle ramp all follow their
 * container.
 *
 * `max(820px, 100svh)` is copied from `.hero` on the homepage rather than
 * invented, because the client asked for this screen to behave like that one.
 * svh rather than vh so a mobile browser's collapsing toolbar does not make
 * the first screen taller than the glass it is shown on.
 *
 * `align-content: center` because a full-height section with top-anchored type
 * leaves the composition sitting in the upper fifth with nothing under it. The
 * homepage can anchor to the top — it has a field of glyphs filling the rest.
 * This screen has one title block, so it belongs in the middle of the frame.
 * The padding above is still header-height plus, so the optical centre lands
 * slightly below the true one, which is where it should be with a masthead
 * overhead. */
.contact-hero {
  /* THE FIRST SCREEN IS THE HERO PLUS THE REVIEWS, NOT THE HERO ALONE.
   *
   * The client asked for the statement and the scrolling reviews to be on the
   * first screen together. Making the hero 100svh put the reviews back below
   * the fold — the two requests pull against each other and the reviews win,
   * because a full-bleed painting is worth less than a client actually reading
   * what other clients said.
   *
   * So the hero takes the screen MINUS the review band, and the two of them
   * add up to it. The subtraction is not a guess: the band is its own two
   * padding clamps plus a content block that measures 272px at 1024, 299 at
   * 1440 and 306 from 1920 up, where both clamps have maxed out. Reproducing
   * the clamps here keeps the arithmetic correct as the viewport moves instead
   * of correct at one width — measured 368 / 399 / 438 against a computed
   * 368 / 405 / 438.
   *
   * The 430px floor is for short windows where the two genuinely do not fit;
   * there the page scrolls, which is the honest outcome rather than crushing
   * the title. Below 961px the existing compact rules still own the hero — a
   * phone should not spend a whole screen on a title before the enquiry form.
   */
  min-height: max(820px, 100svh);
  display: grid;
  /* UPPER HALF, NOT THE MIDDLE — 2026-08-08.
   *
   * `center` was right while this section was 900px tall and held nothing else.
   * Once it became the whole screen with the reviews pinned along the bottom,
   * centring put the title at 46% and left the top half of a 4K screen holding
   * nothing but sky. The client's words: it should be in the upper half.
   *
   * `start` with a deliberate top offset rather than `center`, which is the
   * homepage's idiom for a full-height opening — `.hero-inner` anchors its type
   * to the top with `clamp(152px, 17vh, 180px)` of padding rather than floating
   * it. 9vh here instead of 17: this block is shorter than the homepage's and
   * has the review band beneath it, so it wants to sit a little higher than
   * halfway rather than a lot. */
  align-content: start;
  @media (min-width: 961px) {
    /* THE PAINTING IS THE SCREEN, AND THE REVIEWS SIT ON IT.
     *
     * The previous attempt gave the reviews their own strip by shortening the
     * hero, which put them on the first screen but stopped the artwork being
     * the screen's background — the client's words: it should cover all three
     * elements, not just the word Contact.
     *
     * So the hero takes the whole screen again, and the review band is pulled
     * up over its lower third by exactly its own height (see
     * .testimonials-section below). Two elements, one screen, one painting
     * behind both. */
    min-height: max(560px, 100svh);
  }
  position: relative;
  isolation: isolate;
  overflow: hidden;
  padding: calc(var(--header-height) + clamp(34px, 9vh, 130px))
 var(--contact-gutter)
 clamp(34px, 2.9vw, 52px);
  color: var(--contact-ink);
  background: var(--contact-paper);
  @media (max-width: 1180px) {
    padding: calc(var(--header-height) + clamp(34px, 3.4vw, 50px))
    var(--contact-gutter)
    clamp(34px, 3.6vw, 52px);
  }
}

/* ------------------------------------------------------------
   Hero ground — the People construction, applied here

   Same fault, same fix. The ground was flat paper with two near-invisible
   gradients over it, so a route that opens on a dark spatial plate had nothing
   for that plate to sit in. It is now a colour field, frosted on the left where
   the copy runs and sharp on the right where the plate does, ramping between.

   Different painting on purpose: `luminous-reserve` here against
   `equilibrium-field` on People. The two routes share a system, not a picture.

   The frost is three pre-blurred images rather than `backdrop-filter`, and that
   is measured, not preference — the People build note records 20–30fps for the
   live-blur version against 115fps with no frost, because a backdrop filter
   re-samples and re-blurs its backdrop on every frame the layer moves. Blurring
   a static image is work that can be done once. All three levels are 9KB.

   Registration is the fragile part: the blurred copies must frame identically
   to the sharp field or the ramp shows a double image, so all four read one
   `--field-focus` property and a responsive override cannot move one alone. */

.contact-hero {
  /* Right-anchored, not centred. The artwork has a subject now — a globe — and
     `cover` has to take its overflow off the left, where there is nothing but
     paper, instead of splitting it between both edges and slicing the sphere. */
  --field-focus: 100% center;
  /* HOW BIG THE PAINTING IS DRAWN — one property, four layers.
   *
   * The sharp field and the three pre-blurred copies all sized themselves
   * `cover`, so enlarging one would have slid it out of register with the blur
   * ramp masked over it. Same reasoning as --field-focus above: the layers move
   * together or not at all.
   *
   * `auto 120%` is 120% OF COVER, not 120% of the box, and that equivalence is
   * why it is written this way round. The artwork is 2400x1180, a ratio of
   * 2.03; every viewport this rule applies to is between 1.6 and 1.83, all
   * narrower than the picture, so `cover` is already deciding by HEIGHT and
   * letting the width overflow. Driving the height therefore scales cover
   * exactly, and cannot leave an uncovered edge at any window shape. */
  --field-size: auto 120%;
}

/* THE HERO RESERVES THE BAND'S HEIGHT, WHICH IS WHAT MAKES THE OVERLAP
 * IMPOSSIBLE RATHER THAN UNLIKELY.
 *
 * The band is pulled up by exactly `--contact-reviews-h`. Reserve that plus a
 * gap as bottom padding here and the arithmetic closes in both directions:
 *
 *   - when 100svh decides the hero's height, it is only because it exceeds
 *     content + padding — so it exceeds content + band + 24 as well, and the
 *     band lands more than 24px below the title block;
 *   - when the content decides it, the hero is exactly content + band + 24, so
 *     lifting the band by its own height parks its top edge 24px below the
 *     title block and not one pixel higher.
 *
 * That is why this is a `max()` against the ordinary padding rather than a
 * measured number: no viewport, no quotation length and no amount of browser
 * furniture can make it wrong, which a tuned figure could not promise. A short
 * window now grows the hero and scrolls — the outcome the file's own note
 * already called the honest one — instead of drawing two things in one place.
 *
 * Only above 961px, because below it there is no overlay to reserve for. */
@media (min-width: 961px) {
  .contact-hero {
    padding-bottom: max(clamp(34px, 3.6vw, 52px), calc(var(--contact-reviews-h) + 24px));
  }
}

.contact-hero__field,
.contact-hero__frost,
.contact-hero__settle {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

.contact-hero__field {
  z-index: -3;
  background: url("assets/contact-field.jpg") var(--field-focus) / var(--field-size) no-repeat;
}

/* The grain is what separates a painting from a gradient on a screen, and it is
   the overlay the homepage art and the People opening both carry. */
.contact-hero__field::after {
  content: "";
  position: absolute;
  inset: 0;
  opacity: 0.15;
  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");
}

.contact-hero__frost {
  z-index: -2;
}

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

.contact-hero__frost span:nth-child(1) {
  background-image: url("assets/contact-field-soft.jpg");
  mask-image: linear-gradient(90deg, #000 0%, #000 42%, transparent 68%);
  -webkit-mask-image: linear-gradient(90deg, #000 0%, #000 42%, transparent 68%);
}

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

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

/* A thin scrim, not a wash — the point of the frost is that legibility no
   longer costs colour. */
.contact-hero__frost::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(
    90deg,
    rgba(246, 243, 235, 0.4) 0%,
    rgba(246, 243, 235, 0.26) 24%,
    rgba(246, 243, 235, 0.13) 56%,
    rgba(246, 243, 235, 0) 78%
  );
}

/* A light scrim under the site header. The nav is translucent and sits across
   the top of whatever artwork is behind it; this image happens to fall from
   luminance 237 on the left to 86 on the right along that band, which left the
   "Contact" item barely readable. Solving it in the artwork would mean
   re-commissioning the picture every time, so it is solved here instead and the
   next image inherits the fix. */
.contact-hero__masthead {
  position: absolute;
  z-index: -1;
  top: 0;
  right: 0;
  left: 0;
  height: calc(var(--header-height) + 34px);
  pointer-events: none;
  /* Two axes, because the fault has two. Vertically it fades out below the
     header. Horizontally it strengthens toward the right, where this artwork
     drops from luminance 237 to 86 and took the copper "Contact" item down to
     2.7:1 — the one nav item that is not near-black and so the only one with no
     margin to give. */
  background:
    linear-gradient(90deg, rgba(247, 245, 239, 0) 34%, rgba(247, 245, 239, 0.5) 78%, rgba(247, 245, 239, 0.62) 100%),
    linear-gradient(180deg, rgba(247, 245, 239, 0.84) 0%, rgba(247, 245, 239, 0.62) 46%, rgba(247, 245, 239, 0) 100%);
  /* The horizontal component has no vertical falloff of its own, so on its own it
     paints a uniformly opaque band the full height of this element and then stops
     dead — a white stripe straight across the artwork. Masking the whole layer
     vertically makes both components fade out together. */
  mask-image: linear-gradient(180deg, #000 0%, rgba(0, 0, 0, 0.62) 48%, transparent 100%);
  -webkit-mask-image: linear-gradient(180deg, #000 0%, rgba(0, 0, 0, 0.62) 48%, transparent 100%);
}

/* The hero used to end on a hard edge against the testimonials. It now settles
   into the paper the rest of the route is set on. */
.contact-hero__settle {
  z-index: -1;
  background: linear-gradient(180deg, rgba(244, 241, 233, 0) 68%, rgba(244, 241, 233, 0.66) 87%, var(--contact-paper) 100%);
}

/* Without mask support the blurred copies would cover the field entirely and
   the sharp half would be gone, so that path drops them and restores a wash. */
@supports not ((mask-image: linear-gradient(#000, transparent)) or (-webkit-mask-image: linear-gradient(#000, transparent))) {
  .contact-hero__frost span {
    display: none;
  }

  .contact-hero__frost::after {
    background: linear-gradient(
      90deg,
      rgba(246, 243, 235, 0.93) 0%,
      rgba(246, 243, 235, 0.82) 26%,
      rgba(246, 243, 235, 0.5) 46%,
      rgba(246, 243, 235, 0.12) 66%,
      rgba(26, 18, 28, 0.08) 100%
    );
  }
}

/* The copper bar survives the change of ground and moves up a layer so the
   frost cannot bury it. The vertical column rule that used to sit beside it is
   gone: it divided the copy from the spatial plate, and once the plate was
   removed and the hero became a single column it was dividing nothing. */
.contact-hero__settle::after {
  content: "";
  width: 82px;
  height: 3px;
  position: absolute;
  left: var(--contact-gutter);
  bottom: 0;
  background: var(--contact-copper);
}

.contact-hero__inner {
  width: 100%;
  max-width: 1480px;
  /* THE FLOOR CAME DOWN FROM 286px, and only short windows can tell.
     32vh reaches 286 at a 894px window, so every screen taller than that is
     untouched to the pixel. Below it, 286px of box was being held open for
     about 184px of type — and on an iPad in Safari that slack is the difference
     between the whole first review card being on the first screen and its
     reviewer's name being cut off by the fold. Measured at 1194x664: the card's
     foot moves from 715 to 641 against a 664px window. */
  min-height: clamp(210px, 32vh, 366px);
  margin: 0 auto;
  display: grid;
  /* One column. The right of the frame was holding a plate; it now holds the
     picture, which is the point of the artwork being there at all. */
  grid-template-columns: minmax(0, 1fr);
  align-items: center;
}

.contact-hero__content {
  max-width: min(100%, 620px);
  position: relative;
  z-index: 2;
  padding-top: 10px;
}

.contact-kicker {
  margin: 0 0 clamp(16px, 1.38vw, 26px);
  display: flex;
  align-items: center;
  gap: 16px;
  /* Deeper than `--contact-copper`, and hero-scoped. b62 deepened the ground for
     gravitas, which cost the kicker contrast — at 10px bold it is small text and
     needs 4.5:1, and the page copper only reached 3.6:1 against the new field.
     The lighter copper stays correct everywhere it sits on paper. */
  color: #5c3122;
  font-family: var(--sans);
  font-size: clamp(10px, 0.5952vw, 18px);
  font-weight: 700;
  letter-spacing: 0.22em;
  line-height: 1;
  text-transform: uppercase;
  @media (max-width: 1180px) {
    margin: 0 0 clamp(27px, 3.2vw, 44px);
  }
}

.contact-kicker::before {
  content: "";
  width: 34px;
  height: 1px;
  background: currentColor;
}

.contact-hero h1 {
  margin: 0;
  font-family: var(--serif);
  font-size: clamp(54px, 4.62vw, 90px);
  font-weight: 400;
  letter-spacing: -0.072em;
  line-height: 0.82;
  @media (max-width: 1180px) {
    font-size: clamp(54px, 5.7vw, 90px);
  }
}

.contact-hero__statement {
  max-width: clamp(406px, 34.375vw, 660px);
  margin: clamp(22px, 1.9vw, 34px) 0 0;
  color: #2c2925;
  font-family: var(--serif);
  font-size: clamp(18px, 1.18vw, 23px);
  font-weight: 400;
  letter-spacing: -0.022em;
  line-height: 1.15;
  text-wrap: pretty;
  @media (max-width: 1180px) {
    max-width: 660px;
    margin: clamp(42px, 5vw, 68px) 0 0;
    font-size: clamp(22px, 1.85vw, 29px);
  }
}

.contact-hero__actions {
  margin-top: clamp(20px, 1.6vw, 30px);
  display: flex;
  align-items: center;
  gap: clamp(18px, 1.5625vw, 30px);
  @media (max-width: 1180px) {
    margin-top: clamp(34px, 4vw, 52px);
    gap: 30px;
  }
}

.contact-primary-action {
  min-width: 218px;
  min-height: 44px;
  padding: 11px 14px 11px 17px;
  display: inline-flex;
  align-items: center;
  justify-content: space-between;
  gap: clamp(20px, 1.66667vw, 32px);
  border: 1px solid var(--contact-ink);
  color: var(--contact-paper);
  background-color: var(--contact-ink);
  background-image: var(--contact-grain), linear-gradient(168deg, #2b2b28 0%, #1d1d1a 52%, #121210 100%);
  background-size: 140px 140px, auto;
  background-blend-mode: soft-light, normal;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  transition: color 180ms ease, background-color 180ms ease;
  @media (max-width: 1180px) {
    gap: 32px;
  }
}

.contact-primary-action:hover,
.contact-primary-action:focus-visible {
  color: var(--contact-ink);
  background: transparent;
}

.contact-primary-action svg,
.contact-section-link svg,
.office-map > a svg,
.enquiry-form__submit button svg {
  width: 18px;
  height: 18px;
  flex: 0 0 auto;
  fill: none;
  stroke: currentColor;
  stroke-linecap: square;
  stroke-linejoin: miter;
  stroke-width: 1.35;
}

.contact-text-action {
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  color: var(--contact-ink);
  border-bottom: 1px solid rgba(29, 29, 26, 0.48);
  font-size: clamp(10px, 0.5952vw, 18px);
  font-weight: 650;
  letter-spacing: 0.04em;
  transition: color 180ms ease, border-color 180ms ease;
}

.contact-text-action:hover,
.contact-text-action:focus-visible {
  color: var(--contact-copper);
  border-color: currentColor;
}

/* Static spatial globe: no Canvas, requestAnimationFrame or animation loop. */
.contact-hero__inner {
  width: 100%;
  max-width: 1480px;
  /* THE FLOOR CAME DOWN FROM 286px, and only short windows can tell.
     32vh reaches 286 at a 894px window, so every screen taller than that is
     untouched to the pixel. Below it, 286px of box was being held open for
     about 184px of type — and on an iPad in Safari that slack is the difference
     between the whole first review card being on the first screen and its
     reviewer's name being cut off by the fold. Measured at 1194x664: the card's
     foot moves from 715 to 641 against a 664px window. */
  min-height: clamp(210px, 32vh, 366px);
  margin: 0 auto;
  display: grid;
  /* One column. The right of the frame was holding a plate; it now holds the
     picture, which is the point of the artwork being there at all. */
  grid-template-columns: minmax(0, 1fr);
  align-items: center;
}

.contact-hero__content {
  max-width: min(100%, 620px);
  position: relative;
  z-index: 2;
  padding-top: 10px;
}

.contact-kicker {
  margin: 0 0 clamp(16px, 1.38vw, 26px);
  display: flex;
  align-items: center;
  gap: 16px;
  /* Deeper than `--contact-copper`, and hero-scoped. b62 deepened the ground for
     gravitas, which cost the kicker contrast — at 10px bold it is small text and
     needs 4.5:1, and the page copper only reached 3.6:1 against the new field.
     The lighter copper stays correct everywhere it sits on paper. */
  color: #5c3122;
  font-family: var(--sans);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.22em;
  line-height: 1;
  text-transform: uppercase;
  @media (max-width: 1180px) {
    margin: 0 0 clamp(27px, 3.2vw, 44px);
  }
}

.contact-kicker::before {
  content: "";
  width: 34px;
  height: 1px;
  background: currentColor;
}

.contact-hero h1 {
  margin: 0;
  font-family: var(--serif);
  font-size: clamp(54px, 4.62vw, 90px);
  font-weight: 400;
  letter-spacing: -0.072em;
  line-height: 0.82;
  @media (max-width: 1180px) {
    font-size: clamp(54px, 5.7vw, 90px);
  }
}

.contact-hero__statement {
  max-width: clamp(406px, 34.375vw, 660px);
  margin: clamp(22px, 1.9vw, 34px) 0 0;
  color: #2c2925;
  font-family: var(--serif);
  font-size: clamp(18px, 1.18vw, 23px);
  font-weight: 400;
  letter-spacing: -0.022em;
  line-height: 1.15;
  text-wrap: pretty;
  @media (max-width: 1180px) {
    max-width: 660px;
    margin: clamp(42px, 5vw, 68px) 0 0;
    font-size: clamp(22px, 1.85vw, 29px);
  }
}

.contact-hero__actions {
  margin-top: clamp(20px, 1.6vw, 30px);
  display: flex;
  align-items: center;
  gap: clamp(18px, 1.5625vw, 30px);
  @media (max-width: 1180px) {
    margin-top: clamp(34px, 4vw, 52px);
    gap: 30px;
  }
}

.contact-primary-action {
  min-width: 218px;
  min-height: 44px;
  padding: 11px 14px 11px 17px;
  display: inline-flex;
  align-items: center;
  justify-content: space-between;
  gap: clamp(20px, 1.66667vw, 32px);
  border: 1px solid var(--contact-ink);
  color: var(--contact-paper);
  background-color: var(--contact-ink);
  background-image: var(--contact-grain), linear-gradient(168deg, #2b2b28 0%, #1d1d1a 52%, #121210 100%);
  background-size: 140px 140px, auto;
  background-blend-mode: soft-light, normal;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1);
  font-size: clamp(10px, 0.5952vw, 18px);
  font-weight: 700;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  transition: color 180ms ease, background-color 180ms ease;
  @media (max-width: 1180px) {
    gap: 32px;
  }
}

.contact-primary-action:hover,
.contact-primary-action:focus-visible {
  color: var(--contact-ink);
  background: transparent;
}

.contact-primary-action svg,
.contact-section-link svg,
.office-map > a svg,
.enquiry-form__submit button svg {
  width: 18px;
  height: 18px;
  flex: 0 0 auto;
  fill: none;
  stroke: currentColor;
  stroke-linecap: square;
  stroke-linejoin: miter;
  stroke-width: 1.35;
}

.contact-text-action {
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  color: var(--contact-ink);
  border-bottom: 1px solid rgba(29, 29, 26, 0.48);
  font-size: 12px;
  font-weight: 650;
  letter-spacing: 0.04em;
  transition: color 180ms ease, border-color 180ms ease;
}

.contact-text-action:hover,
.contact-text-action:focus-visible {
  color: var(--contact-copper);
  border-color: currentColor;
}

/* Static spatial globe: no Canvas, requestAnimationFrame or animation loop. */
/* Where we are, drawn into the field rather than placed on top of it.

   This replaces the dark spatial plate. The plate was an object sitting on the
   page; this is a mark *in* the picture — which is the whole difference the
   user was asking for. The coastline is projected from real longitude and
   latitude and the Sydney pin uses the same transform, so the point is on the
   city rather than near it.

   It has to stay at the edge of visibility. A map that announces itself becomes
   an infographic; at this weight it reads as something you notice second, after
   the colour, which is what makes it feel found rather than placed. */
.contact-place {
  position: relative;
  justify-self: end;
  align-self: center;
  width: min(100%, 560px);
}

.contact-place__map {
  width: 100%;
  height: auto;
  display: block;
  overflow: visible;
}

/* Two strokes, not one: a pale line carries the coast where the field is dark,
   and a dark line carries it where the field is light. Neither alone survives a
   ground that changes underneath it. */
.contact-place__coast {
  fill: rgba(255, 255, 255, 0.05);
  stroke: rgba(255, 255, 255, 0.5);
  stroke-width: 1.6;
  stroke-linejoin: round;
  paint-order: stroke fill;
  filter: drop-shadow(0 0 1.5px rgba(58, 40, 30, 0.45));
}

/* A leader from the pin down to the city's name. The label was floating free of
   the point it names; a hairline is the cartographer's answer and it is the
   site's own device. */
.contact-place__leader {
  stroke: rgba(120, 86, 50, 0.42);
  stroke-width: 1.1;
  stroke-dasharray: 2 4;
}

.contact-place__ring {
  fill: none;
  stroke: rgba(255, 252, 246, 0.72);
  stroke-width: 1.2;
}

.contact-place__halo {
  fill: rgba(190, 132, 74, 0.22);
  animation: contact-pin-breathe 5.5s ease-in-out infinite;
  transform-origin: 911.8px 578.6px;
}

.contact-place__pin {
  fill: #a4661f;
  stroke: rgba(255, 251, 244, 0.95);
  stroke-width: 1.8;
}

@keyframes contact-pin-breathe {
  0%, 100% { transform: scale(0.82); opacity: 0.75; }
  50% { transform: scale(1.12); opacity: 0.3; }
}

/* The city named in the site's own label register, set against the pin. */
/* Right edge parked under the pin, which sits at 91.2% of the map's width, so
   the leader lands on the name instead of near it. */
.contact-place__label {
  margin: 6px 0 0;
  padding-right: 8.8%;
  display: flex;
  flex-direction: column;
  gap: 5px;
  text-align: right;
  align-items: flex-end;
}

.contact-place__label > span:first-child {
  color: var(--contact-ink);
  font-family: var(--serif);
  font-size: clamp(19px, 1.25vw, 24px);
  letter-spacing: -0.012em;
  @media (max-width: 1180px) {
    font-size: clamp(19px, 1.5vw, 24px);
  }
}

.contact-place__coord {
  color: #6b6259;
  font-family: var(--sans);
  font-size: clamp(9.5px, 0.5655vw, 17.1px);
  font-weight: 700;
  letter-spacing: 0.18em;
  text-transform: uppercase;
}

@media (prefers-reduced-motion: reduce) {
  .contact-place__halo {
    animation: none;
  }
}

.contact-hero__inner {
  width: 100%;
  max-width: 1480px;
  /* THE FLOOR CAME DOWN FROM 286px, and only short windows can tell.
     32vh reaches 286 at a 894px window, so every screen taller than that is
     untouched to the pixel. Below it, 286px of box was being held open for
     about 184px of type — and on an iPad in Safari that slack is the difference
     between the whole first review card being on the first screen and its
     reviewer's name being cut off by the fold. Measured at 1194x664: the card's
     foot moves from 715 to 641 against a 664px window. */
  min-height: clamp(210px, 32vh, 366px);
  margin: 0 auto;
  display: grid;
  /* One column. The right of the frame was holding a plate; it now holds the
     picture, which is the point of the artwork being there at all. */
  grid-template-columns: minmax(0, 1fr);
  align-items: center;
}

.contact-hero__content {
  max-width: min(100%, 620px);
  position: relative;
  z-index: 2;
  padding-top: 10px;
}

.contact-kicker {
  margin: 0 0 clamp(16px, 1.38vw, 26px);
  display: flex;
  align-items: center;
  gap: 16px;
  /* Deeper than `--contact-copper`, and hero-scoped. b62 deepened the ground for
     gravitas, which cost the kicker contrast — at 10px bold it is small text and
     needs 4.5:1, and the page copper only reached 3.6:1 against the new field.
     The lighter copper stays correct everywhere it sits on paper. */
  color: #5c3122;
  font-family: var(--sans);
  font-size: clamp(10px, 0.5952vw, 18px);
  font-weight: 700;
  letter-spacing: 0.22em;
  line-height: 1;
  text-transform: uppercase;
  @media (max-width: 1180px) {
    margin: 0 0 clamp(27px, 3.2vw, 44px);
  }
}

.contact-kicker::before {
  content: "";
  width: 34px;
  height: 1px;
  background: currentColor;
}

.contact-hero h1 {
  margin: 0;
  font-family: var(--serif);
  font-size: clamp(54px, 4.62vw, 90px);
  font-weight: 400;
  letter-spacing: -0.072em;
  line-height: 0.82;
  @media (max-width: 1180px) {
    font-size: clamp(54px, 5.7vw, 90px);
  }
}

.contact-hero__statement {
  max-width: clamp(406px, 34.375vw, 660px);
  margin: clamp(22px, 1.9vw, 34px) 0 0;
  color: #2c2925;
  font-family: var(--serif);
  font-size: clamp(18px, 1.18vw, 23px);
  font-weight: 400;
  letter-spacing: -0.022em;
  line-height: 1.15;
  text-wrap: pretty;
  @media (max-width: 1180px) {
    max-width: 660px;
    margin: clamp(42px, 5vw, 68px) 0 0;
    font-size: clamp(22px, 1.85vw, 29px);
  }
}

.contact-hero__actions {
  margin-top: clamp(20px, 1.6vw, 30px);
  display: flex;
  align-items: center;
  gap: clamp(18px, 1.5625vw, 30px);
  @media (max-width: 1180px) {
    margin-top: clamp(34px, 4vw, 52px);
    gap: 30px;
  }
}

.contact-primary-action {
  min-width: 218px;
  min-height: 44px;
  padding: 11px 14px 11px 17px;
  display: inline-flex;
  align-items: center;
  justify-content: space-between;
  gap: clamp(20px, 1.66667vw, 32px);
  border: 1px solid var(--contact-ink);
  color: var(--contact-paper);
  background-color: var(--contact-ink);
  background-image: var(--contact-grain), linear-gradient(168deg, #2b2b28 0%, #1d1d1a 52%, #121210 100%);
  background-size: 140px 140px, auto;
  background-blend-mode: soft-light, normal;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1);
  font-size: clamp(10px, 0.5952vw, 18px);
  font-weight: 700;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  transition: color 180ms ease, background-color 180ms ease;
  @media (max-width: 1180px) {
    gap: 32px;
  }
}

.contact-primary-action:hover,
.contact-primary-action:focus-visible {
  color: var(--contact-ink);
  background: transparent;
}

.contact-primary-action svg,
.contact-section-link svg,
.office-map > a svg,
.enquiry-form__submit button svg {
  width: 18px;
  height: 18px;
  flex: 0 0 auto;
  fill: none;
  stroke: currentColor;
  stroke-linecap: square;
  stroke-linejoin: miter;
  stroke-width: 1.35;
}

.contact-text-action {
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  color: var(--contact-ink);
  border-bottom: 1px solid rgba(29, 29, 26, 0.48);
  font-size: 12px;
  font-weight: 650;
  letter-spacing: 0.04em;
  transition: color 180ms ease, border-color 180ms ease;
}

.contact-text-action:hover,
.contact-text-action:focus-visible {
  color: var(--contact-copper);
  border-color: currentColor;
}

/* Static spatial globe: no Canvas, requestAnimationFrame or animation loop. */
.contact-orbit {
  min-height: clamp(326px, 27.6042vw, 530px);
  position: relative;
  overflow: hidden;
  border: 1px solid rgba(244, 241, 233, 0.17);
  color: #f4efe5;
  background:
    linear-gradient(145deg, rgba(124, 100, 128, 0.52), transparent 52%),
    linear-gradient(22deg, rgba(151, 83, 57, 0.42), transparent 48%),
    #28242b;
  box-shadow: 0 28px 70px rgba(39, 31, 37, 0.14);
  @media (max-width: 1180px) {
    min-height: 530px;
  }
}

/* ------------------------------------------------------------
   Section headings
   ------------------------------------------------------------ */

.contact-section-heading {
  /* Tied to the heading's scale, not fixed: 25px was 0.39em under a 66px heading and
     would be 0.57em under a 44px one. Same reason as the homepage's section rule. */
  padding-bottom: clamp(12px, 1.13vw, 17px);
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: end;
  gap: clamp(21px, 1.77083vw, 34px);
  border-bottom: 1px solid var(--contact-line);
  @media (max-width: 1180px) {
    gap: 34px;
  }
}

/* The site's section-heading scale, which this was not on.
 *
 * It was clamp(45px, 4.5vw, 66px) — 64.8px at 1440, the size of a page title, applied
 * to a four-word sentence. "Contact" works as a 136px h1 because it is one word used as
 * a graphic; "Contact our Sydney office" at 64.8px is not a display word, it is a slab
 * of type 570px wide heading a section whose content is a phone number and a form.
 *
 * The h1 above it is deliberately an outlier and is not the right thing to size against.
 * These are the same section markers the homepage carries, so they take the same scale:
 * clamp(30px, 2.9vw, 44px). */
.contact-section-heading h2 {
  margin: 0;
  color: var(--contact-ink);
  font-family: var(--serif);
  font-size: clamp(30px, 2.29167vw, 44px);
  font-weight: 400;
  letter-spacing: -0.05em;
  line-height: 1;
  @media (max-width: 1180px) {
    font-size: clamp(30px, 2.9vw, 44px);
  }
}

.contact-section-link {
  min-height: 42px;
  padding: 8px 0;
  display: inline-flex;
  align-items: center;
  gap: 14px;
  color: var(--contact-copper);
  font-size: clamp(9px, 0.5357vw, 16.2px);
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

.contact-section-link svg,
.office-map > a svg {
  transition: transform 220ms cubic-bezier(0.22, 1, 0.36, 1);
}

.contact-section-link:hover svg,
.contact-section-link:focus-visible svg,
.office-map > a:hover svg,
.office-map > a:focus-visible svg {
  transform: translate(2px, -2px);
}

/* ------------------------------------------------------------
   Testimonials
   ------------------------------------------------------------ */

/* Was a flat `--contact-paper-deep` slab — a second uniform beige butted against
   the hero's, which is what made this band read as a panel dropped on the page
   rather than part of it. It now sits on the route's own paper, grained, with
   the faintest continuation of the hero's palette across the top so the colour
   hands over instead of stopping at a line. */
/* A TABLET'S EXTRA AIR IS CONDITIONAL ON HAVING THE HEIGHT FOR IT.
 *
 * Three rules above roughly double their spacing below 1180px — the kicker's
 * bottom margin, the statement's top margin and the actions' top margin.
 * Measured, they come to a 288px title block at 1180 against 189px on the
 * desktop side of the same breakpoint, and that extra 99px is right on a tablet
 * held UPRIGHT, which is what it was written for: 1180px of height to spend.
 *
 * Turned sideways in Safari the same tablet has about 650px, and 99px of it was
 * going into the gaps between four lines of type while the first review card
 * was pushed off the bottom of the screen — against the client's own
 * requirement that the statement and the reviews share the first screen.
 *
 * The values below are NOT new numbers: they are the desktop spacings from
 * those same three rules, reused. The height query is the whole of the change.
 * An iPad in portrait is 1180px tall and keeps its air; the same iPad in
 * landscape with the browser's furniture on it does not have the height, and
 * stops pretending it does. */
@media (min-width: 961px) and (max-width: 1180px) and (max-height: 760px) {
  .contact-kicker {
    margin-bottom: clamp(16px, 1.38vw, 26px);
  }

  .contact-hero__statement {
    margin-top: clamp(22px, 1.9vw, 34px);
  }

  .contact-hero__actions {
    margin-top: clamp(20px, 1.6vw, 30px);
  }
}

.testimonials-section {
  padding: clamp(10px, 1.1vw, 20px) var(--contact-gutter) clamp(56px, 4vw, 76px);
  position: relative;
  isolation: isolate;
  color: var(--contact-ink);
  background-color: #f2efe7;
  background-image: var(--contact-grain);
  background-size: 190px 190px;
  background-blend-mode: soft-light;
  /* ON THE PAINTING, NOT UNDER IT — 2026-08-08, desktop only.
   *
   * The band is lifted by its own height so it costs the flow nothing: the
   * hero is 100svh, this sits over its lower part, and the section after it
   * still begins exactly one screen down. The negative margin therefore has to
   * equal the height, and the height is its two padding clamps plus a content
   * block that measures 272px at 1024, 299 at 1440 and 306 from 1920 up.
   *
   * The paper and its grain go, because they are what was hiding the artwork.
   * The review cards carry their own light ground, so the words stay legible
   * over the pale left half where they sit; the hero's settle ramp is what
   * keeps the right half from competing with them. */
  /* The three height expressions this used to carry now live on .contact-page —
     the hero has to read them too. Nothing else about the overlay changed. */
  @media (min-width: 961px) {
    /* Extra space below the larger focal card lifts the complete ribbon up through
       the hero, bringing it closer to the Contact title without shifting the office. */
    padding-bottom: clamp(80px, 5.7vw, 104px);
    margin-top: calc(-1 * var(--contact-reviews-h));
    background-color: transparent;
    background-image: none;
    z-index: 1;
  }
  @media (max-width: 1180px) {
    padding: clamp(10px, 1.4vw, 22px) var(--contact-gutter) clamp(56px, 5vw, 76px);
    @media (min-width: 961px) {
      padding-bottom: clamp(80px, 5.7vw, 104px);
    }
  }
}

.testimonials-section::before {
  content: "";
  position: absolute;
  z-index: -1;
  inset: 0;
  pointer-events: none;
  background:
    radial-gradient(1400px 460px at 82% -8%, rgba(120, 94, 116, 0.14), transparent 72%),
    radial-gradient(1100px 380px at 12% 104%, rgba(150, 118, 80, 0.09), transparent 70%);
  /* Feathered at both ends for the same reason as `.people-directory::before`:
     an ellipse centred outside the box is at full strength on the box's edge. */
  mask-image: linear-gradient(180deg, transparent 0, #000 170px, #000 calc(100% - 130px), transparent 100%);
  -webkit-mask-image: linear-gradient(180deg, transparent 0, #000 170px, #000 calc(100% - 130px), transparent 100%);
  /* Two radial tints drawn to give a flat paper band some depth. Over the
     painting they are haze on top of haze, so they come off wherever the band
     is transparent. */
  @media (min-width: 961px) {
    display: none;
  }
}


/* The quote is free to exceed `min-height`, and when it does its last line
   landed 2-4px above the controls' full-width rule — a 39px serif descender
   effectively sitting on a hairline. The floor gives the longest quote the same
   clearance the short ones get from the min-height. */










/* ------------------------------------------------------------
   Sydney consultation desk
   ------------------------------------------------------------ */

/* Was `--contact-ink`, a flat near-black — precisely the fill the client's rule
   forbids. It keeps its darkness, because the page needs a close and the
   light-light-dark rhythm is right, but it is now a *painted* dark: the route's
   own field, heavily deepened, plus the same grain the rest of the site carries.
   Dark and flat is a slab; dark with material is a ground. */
/* THE HAIRLINE FIELD — a ruled ground rather than a printed one.
 *
 * A REPEATING GRADIENT, NOT A TILE. A tiled SVG of drawn lines is the obvious
 * way and it is the one that failed on this site before: the strokes land on
 * the tile's own edges, the halves get clipped, and the seams read as gaps in
 * the pattern at exactly the spacing of the tile. A repeating-linear-gradient
 * has no tile and therefore cannot have a seam — it is continuous by
 * construction, at every width, zoom and pixel ratio.
 *
 * 3px APART, 1px WIDE, BLACK. Dense enough that the eye reads a material and
 * not a set of lines it could count, fine enough that it never competes with
 * the rules the form and the office facts are drawn with. Black on near-black
 * is the whole point: it deepens the ground unevenly, the way ink on laid paper
 * does, instead of laying a pattern on top of it.
 *
 * The faint light in the gap is not decoration — a line of pure black on
 * #16171a at this scale greys into a flat wash on a low-contrast screen. The
 * ~1% lift gives the rhythm something to be a rhythm against. */
.office-section {
  padding: clamp(56px, 3.9vw, 76px) var(--contact-gutter) clamp(60px, 4.3vw, 84px);
  position: relative;
  isolation: isolate;
  color: #f3eee5;
  background-color: #16171a;
  background-image: repeating-linear-gradient(
    180deg,
    rgba(0, 0, 0, 0.62) 0 1px,
    rgba(255, 255, 255, 0.016) 1px 3px
  );
  @media (max-width: 1180px) {
    padding: clamp(56px, 4.6vw, 76px) var(--contact-gutter) clamp(60px, 5.2vw, 84px);
  }
}

.office-section::before {
  content: "";
  position: absolute;
  z-index: -1;
  inset: 0;
  pointer-events: none;
  opacity: 0.5;
  background:
    radial-gradient(1500px 720px at 88% 6%, rgba(126, 84, 132, 0.5), transparent 66%),
    radial-gradient(1200px 620px at 8% 92%, rgba(158, 92, 60, 0.34), transparent 64%),
    radial-gradient(900px 520px at 46% 44%, rgba(70, 84, 130, 0.3), transparent 68%);
  mask-image: linear-gradient(180deg, transparent 0, #000 180px, #000 calc(100% - 140px), transparent 100%);
  -webkit-mask-image: linear-gradient(180deg, transparent 0, #000 180px, #000 calc(100% - 140px), transparent 100%);
}

.office-section::after {
  content: "";
  position: absolute;
  z-index: -1;
  inset: 0;
  pointer-events: none;
  opacity: 0.22;
  mix-blend-mode: soft-light;
  background-image: var(--contact-grain);
  background-size: 170px 170px;
}

.office-section .contact-section-heading {
  border-color: rgba(255, 255, 255, 0.2);
}

.office-section .contact-section-heading h2 {
  color: #f3eee5;
}

/* WORDS LEFT, MAP RIGHT. `align-items: stretch` rather than `start` is the whole
   mechanism behind "make it more compact": the map is a grid item that now runs
   the height of the enquiry and the office facts together, so the ~400px of
   details that used to sit above it inside the right track cost the section
   nothing. The left track is the wider of the two because it carries a
   two-column field grid; the right needs only to be a legible plan. */
.office-layout {
  padding-top: clamp(30px, 2.4vw, 46px);
  display: grid;
  /* 560px IS NOT A NEW NUMBER — it is the container query below, written where
     the width is actually decided. `@container officewords (min-width: 560px)`
     is what turns the three office facts from a stack into a plaque, and this
     grid is what hands the words column its width; leaving the minimum at 460
     meant the two could disagree. They did: at 1194 — iPad Pro 11 landscape,
     the first width above the tablet rules, where the page gutter jumps from
     30px to 66px — the column came out at 539px, twenty-one pixels short, so
     the facts stacked, the column grew ~130px taller, and the map beside it was
     stretched to 459x830 — a portrait void where a plan should be. A WIDER
     screen than the iPad Air, laid out worse. The minimum now guarantees the
     threshold it exists to clear.

     1.174fr AND 1fr ARE THE OLD 1.08 AND 0.92, RESCALED, and the rescaling is
     load-bearing rather than cosmetic. Their ratio is identical, so every width
     where the minimum does not bind lays out to the pixel as before — measured
     651.6/555.0 at 1440 against 651.3/554.8. But once the 560px minimum makes
     the first track inflexible, the second is the only flexible one left, and a
     lone flex factor BELOW 1 is multiplied by itself: at 1194 the map came out
     at 0.92 of the leftover, 402px instead of 438, and stopped 35px short of
     the right gutter that the section's own rule runs to. A flex factor of 1
     takes the remainder exactly. */
  grid-template-columns: minmax(560px, 1.174fr) minmax(300px, 1fr);
  gap: clamp(40px, 4.2vw, 78px);
  align-items: stretch;
  @media (max-width: 1180px) {
    padding-top: clamp(56px, 6vw, 86px);
    gap: clamp(62px, 8vw, 132px);
  }
}

/* The enquiry, then the facts under it. One act — reach us — in one column,
   the primary way first and the fallback beneath. DOM order is visual order at
   every width, so tab order never disagrees with what is on the screen. */
.office-column {
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: clamp(38px, 3.2vw, 62px);
  /* A CONTAINER, BECAUSE THE QUESTION IS ABOUT THIS COLUMN AND NOT THE WINDOW.
     Whether the three office facts fit on one line depends on how wide the words
     column is, and that is not a function of the viewport: at 960 the column is
     the full width of the page and at 961 it is 48% of it, so the SAME viewport
     width means 885px on one side of the breakpoint and 462px on the other. A
     media query has to guess at that and gets it wrong in exactly the range
     where it matters — at 961 the address broke into four lines. Asked of the
     column itself, the rule is right at every width by construction. */
  container-type: inline-size;
  container-name: officewords;
}

/* THREE ACROSS, NOT THREE DOWN — and this is where "more compact" is actually
   paid for. Stacked rows under the form cost 222px of column and pushed the
   section 200px TALLER than the arrangement it replaced, which is the opposite
   of what was asked for. Side by side they cost about 90px, the section comes
   in shorter than it started, and the map beside them stops being a ribbon.
   They also read correctly: three facts of equal standing, on one line, under
   the enquiry — a plaque, not a list. */
/* Stacked is the base and the fallback: a browser without container queries gets
   three facts one under the other, which is correct and merely taller. */
.office-details {
  margin: 0;
  padding-top: 26px;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 22px;
  border-top: 1px solid rgba(255, 255, 255, 0.2);
}

/* 560px is the width at which the address — the long one, about 340px of serif —
   still has room for two lines beside the phone and the email. Below it the three
   stack; above it they are a plaque. */
@container officewords (min-width: 560px) {
  .office-details {
    grid-template-columns: auto auto minmax(0, 1fr);
    gap: clamp(24px, 2vw, 44px);
  }
}

.office-detail {
  min-width: 0;
}

@media (max-width: 960px) {
  .office-details {
    padding-top: 22px;
  }
}

.office-detail dt {
  margin-bottom: 9px;
  color: #c8a66f;
  font-size: clamp(8px, 0.4762vw, 14.4px);
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
}

.office-detail dd {
  min-width: 0;
  margin: 0;
  color: #f3eee5;
  font-family: var(--serif);
  font-size: clamp(20px, 1.35417vw, 26px);
  letter-spacing: -0.022em;
  line-height: 1.15;
  overflow-wrap: anywhere;
  @media (max-width: 1180px) {
    font-size: clamp(20px, 1.65vw, 26px);
  }
}

.office-detail a {
  display: inline;
  background-image: linear-gradient(currentColor, currentColor);
  background-position: 0 100%;
  background-repeat: no-repeat;
  background-size: 0 1px;
  transition: color 180ms ease, background-size 280ms cubic-bezier(0.22, 1, 0.36, 1);
}

.office-detail a:hover,
.office-detail a:focus-visible {
  color: #d4b47d;
  background-size: 100% 1px;
}

.enquiry-form {
  min-width: 0;
  padding: 0;
  color: #f3eee5;
  background: transparent;
}

.enquiry-form__heading {
  padding: 0 0 31px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.2);
}

.enquiry-form__heading > p {
  margin: 0 0 20px;
  color: #c8a66f;
  font-size: clamp(9px, 0.5357vw, 16.2px);
  font-weight: 700;
  letter-spacing: 0.18em;
  text-transform: uppercase;
}

/* Nested inside the office section, under "Contact our Sydney office", and set at 4.2vw
   against that heading's 4.5vw — 93% of its own parent, two levels apart in the document
   and 7% apart on the page. Two 60px serif lines in one section is what made the section
   read as heavy; neither could rank the other. 0.68 of the section heading now, the same
   step the homepage's expertise headings take. */
.enquiry-form__heading h3 {
  max-width: clamp(418px, 35.4167vw, 680px);
  margin: 0;
  font-family: var(--serif);
  font-size: clamp(21px, 1.5625vw, 30px);
  font-weight: 400;
  letter-spacing: -0.047em;
  line-height: 1;
  @media (max-width: 1180px) {
    max-width: 680px;
    font-size: clamp(21px, 2vw, 30px);
  }
}

.enquiry-form__heading > span {
  max-width: clamp(326px, 27.6042vw, 530px);
  margin-top: 20px;
  display: block;
  color: rgba(255, 255, 255, 0.54);
  font-size: 13px;
  letter-spacing: 0.01em;
  line-height: 1.15;
  @media (max-width: 1180px) {
    max-width: 530px;
  }
}

.enquiry-fields {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  column-gap: clamp(21px, 1.77083vw, 34px);
  @media (max-width: 1180px) {
    column-gap: 34px;
  }
}

/* THE RULE BELONGS TO THE CONTROL, NOT TO THE ROW — and that is the fix for the
   thing the client reported, not a tidy-up.
 *
 * It used to sit on `.enquiry-field`, at the bottom of a 98px row. The input's
 * own text ended ~23px above it, and `.enquiry-error` was squeezed into that
 * gap: so "This field is required." appeared ABOVE the line, on the writing
 * line, at a different height from the caret it was talking about. The message
 * looked like something typed into the field.
 *
 * On the control, the line is under the words the visitor writes — which is
 * what a ruled line means — and the message falls below it, in the space
 * between this field and the next. Same rhythm, no reflow when an error
 * appears, because the row keeps its min-height either way. */
.enquiry-field {
  min-width: 0;
  min-height: 98px;
  padding-top: 25px;
}

.enquiry-field--message {
  min-height: clamp(109px, 9.27083vw, 178px);
  grid-column: 1 / -1;
  @media (max-width: 1180px) {
    min-height: 178px;
  }
}

.enquiry-field label {
  display: block;
  color: rgba(255, 255, 255, 0.5);
  font-size: clamp(8px, 0.4762vw, 14.4px);
  font-weight: 700;
  letter-spacing: 0.15em;
  text-transform: uppercase;
}

.enquiry-field label span {
  color: #d0ae75;
}

.enquiry-field input,
.enquiry-field select,
.enquiry-field textarea {
  width: 100%;
  min-width: 0;
  margin-top: 9px;
  padding: 4px 0 12px;
  border: 0;
  border-bottom: 1px solid rgba(255, 255, 255, 0.24);
  border-radius: 0;
  outline: 0;
  color: #f3eee5;
  background: transparent;
  font-family: var(--serif);
  font-size: 19px;
  line-height: 1.15;
  transition: border-color 180ms ease, box-shadow 180ms ease;
}

@media (prefers-reduced-motion: reduce) {
  .enquiry-field input,
  .enquiry-field select,
  .enquiry-field textarea { transition: none; }
}

/* The matter list is the one control whose content is not the visitor's own
   typing, so it is the one that can overflow its own box: at 19px the longest
   option needed 384px in a 339px field and rendered as `Australia-related
   Cross-Border Investı`. The labels were shortened as well; this keeps the
   longest of them inside the field at 768, which is the tightest width. */
.enquiry-field select { font-size: 17px; }

/* GOLD, AND NO BOX. The site ring was applied here as `outline: 2px solid
   #c9613d` at a 5px offset, and on this layout it did three wrong things at
   once: it was orange on a page whose only accent is gold, it drew a rectangle
   around a field that has no other box, and at that offset it ran straight
   through the error message below and within a pixel of the neighbouring
   field's rule — which is the doubled-line the client saw.
 *
 * The focused control's own rule goes gold and gains a second pixel through a
 * box-shadow rather than a thicker border, so nothing reflows. That is a change
 * of colour AND of weight on the one line the visitor is writing on: a real
 * focus signal, and not a colour-only one. */
.enquiry-field input:focus-visible,
.enquiry-field select:focus-visible,
.enquiry-field textarea:focus-visible,
.enquiry-field input:focus,
.enquiry-field select:focus,
.enquiry-field textarea:focus {
  border-bottom-color: #d0ae75;
  box-shadow: 0 1px 0 0 #d0ae75;
}

.enquiry-field select {
  padding-right: 24px;
  color-scheme: dark;
}

.enquiry-field textarea {
  min-height: clamp(65px, 5.52083vw, 106px);
  resize: vertical;
  @media (max-width: 1180px) {
    min-height: 106px;
  }
}

/* NO RED. The palette of this page is gold on near-black; a red rule and a
   salmon message were the only two things on it from another palette, and they
   were the "red line" and "red box" the client asked to be rid of.
 *
 * An invalid field is now said three ways, none of them a colour borrowed from
 * elsewhere: its rule warms to a dimmed gold, its label goes gold, and the
 * message appears under the line with a struck mark in front of it. The mark is
 * a shape, so the state does not rest on colour — and the message itself always
 * names the problem in words, which is what actually carries it. */
.enquiry-field.has-error input,
.enquiry-field.has-error select,
.enquiry-field.has-error textarea {
  border-bottom-color: rgba(208, 174, 117, 0.62);
}

.enquiry-field.has-error label {
  color: #c8a66f;
}

/* Below the rule, in the gap between this field and the next — never in the
   space the visitor is typing into. The min-height reserves that gap whether or
   not there is a message, so nothing on the form moves when one appears. */
.enquiry-error {
  min-height: 15px;
  margin: 8px 0 0;
  display: flex;
  align-items: baseline;
  gap: 7px;
  color: #d4b47d;
  font-size: clamp(9px, 0.5357vw, 16.2px);
  letter-spacing: 0.03em;
  line-height: 1.15;
}

.enquiry-error:not(:empty)::before {
  content: "";
  width: 5px;
  height: 5px;
  flex: none;
  align-self: center;
  border-radius: 50%;
  background: #d0ae75;
}

.enquiry-form__submit {
  padding-top: 16px;
  display: flex;
  align-items: center;
  gap: clamp(12px, 0.8333vw, 16px);
  @media (max-width: 1180px) {
    gap: 16px;
  }
}

/* ── SEND ENQUIRY ──────────────────────────────────────────────────────────
 *
 * The one control on this site that a visitor presses to become a client.
 *
 * IT IS THE FOOTER'S GESTURE NOW, BY REQUEST — 「做成 people 底下那个 contact
 * us 的效果」. `.footer-action` is a hairline outline at rest with a gold fill
 * that sweeps across it on hover, the label inverting to ink as the ground
 * arrives underneath. The visitor meets that button at the bottom of every page
 * on the site before they ever reach this form, so the two primary calls to
 * action being one gesture is worth more than either being distinctive. Every
 * value below is the footer's, deliberately: the same cream hairline, the same
 * #dfbd7e, the same 520ms sweep and 4px arrow.
 *
 * IT IS NOT THE GOLD SLAB FROM TWO REVISIONS AGO. That one was gold AT REST —
 * flat, loud against a near-black section, and the same weight resting, hovered
 * or pressed; the client's word for it was ugly. Here gold is what happens when
 * you point at it, and the resting state is a hairline. The distinction is the
 * whole design: the colour is the response, not the object.
 *
 * WHAT WENT WITH IT: the six-layer dark-glass stack — pane, graduation, bevel,
 * gold rim, sheen and cast. It was carefully built and it photographed well, and
 * on a near-black section it still read as a grey slab a visitor could not be
 * sure was a button. A fill that crosses the control on hover answers that in a
 * way a backdrop blur cannot.
 */
.enquiry-form__submit button {
  min-width: 208px;
  min-height: 54px;
  padding: 14px 18px 14px 22px;
  position: relative;
  isolation: isolate;
  overflow: hidden;
  display: inline-flex;
  align-items: center;
  justify-content: space-between;
  gap: clamp(18px, 1.5625vw, 30px);
  /* Square, like the footer's. A radius on one and not the other is the kind of
     difference that reads as an accident rather than as a decision. */
  border: 1px solid rgba(239, 236, 228, 0.32);
  background: transparent;
  color: #faf7f0;
  cursor: pointer;
  font-size: clamp(12px, 0.7143vw, 16.2px);
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  transition:
    color 260ms ease,
    border-color 260ms ease,
    transform 160ms cubic-bezier(0.22, 1, 0.36, 1);
  @media (max-width: 1180px) {
    gap: 30px;
  }
}

/* The fill. `::before` rather than the footer's extra `<span>`, because this
   button's markup is generated from concepts/style-lab and a pseudo-element
   needs no regeneration — the effect is identical either way. Behind the label
   in stacking order (the button `isolate`s, so z-index -1 stays inside it) and
   inert to the pointer, so it can never intercept the press.

   Anchored right at rest and left on hover, so it enters from the leading edge
   and, when the pointer leaves, retreats the way it came instead of sliding out
   the far side. */
.enquiry-form__submit button::before {
  content: "";
  position: absolute;
  z-index: -1;
  inset: 0;
  pointer-events: none;
  background: #dfbd7e;
  transform: scaleX(0);
  transform-origin: 100% 50%;
  transition: transform 520ms cubic-bezier(0.22, 1, 0.36, 1);
}

.enquiry-form__submit button:hover,
.enquiry-form__submit button:focus-visible {
  /* The fill is gold, so the label becomes the ground rather than paper. */
  color: #241f1a;
  border-color: #dfbd7e;
}

.enquiry-form__submit button:hover::before,
.enquiry-form__submit button:focus-visible::before {
  transform: scaleX(1);
  transform-origin: 0 50%;
}

/* Pressed: it goes down a pixel. The glass had a cast to tighten; this one has
   only its own position to give, which is enough to answer a click. */
.enquiry-form__submit button:active {
  transform: translateY(1px);
}

/* Sending. The control is genuinely disabled while the request is in flight, so
   it goes quiet and the fill stays home — nothing responds to a pointer the
   button is no longer going to answer. */
.enquiry-form__submit button:disabled {
  cursor: default;
  opacity: 0.62;
  transform: none;
}

.enquiry-form__submit button:disabled::before {
  transform: scaleX(0);
}

/* THE ARROW FOLLOWS THE LABEL, and this is the one value that could not be
   carried over unchanged. It used to be gold on a dark pane — the accent on the
   part that points rather than the part that is read. A gold arrow on a gold
   fill is an arrow that disappears at exactly the moment the button is being
   used, so it takes `currentColor` and inverts with the label, as the footer's
   does. */
.enquiry-form__submit button svg {
  color: inherit;
  transition: transform 380ms cubic-bezier(0.22, 1, 0.36, 1);
}

.enquiry-form__submit button:hover svg,
.enquiry-form__submit button:focus-visible svg {
  transform: translateX(4px);
}

/* The keyboard ring. A rectangle around a rectangular control is the control's
   own shape, not the stray box the fields were carrying — and it is gold here,
   like everything else that lights up on this page. */
.enquiry-form__submit button:focus-visible {
  outline: 2px solid rgba(214, 182, 128, 0.85);
  outline-offset: 3px;
}

@media (prefers-reduced-motion: reduce) {
  .enquiry-form__submit button,
  .enquiry-form__submit button::before {
    transition: none;
  }
}

.enquiry-form__submit > p {
  max-width: clamp(250px, 18.75vw, 360px);
  margin: 0;
  color: rgba(255, 255, 255, 0.66);
  font-size: clamp(12px, 0.7143vw, 16.2px);
  letter-spacing: 0.01em;
  line-height: 1.4;
  @media (max-width: 1180px) {
    max-width: 360px;
  }
}

.enquiry-form__submit > p a {
  color: #d4b47d;
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.18em;
}

.enquiry-form__submit > p a:hover,
.enquiry-form__submit > p a:focus-visible {
  color: #f0d7a8;
}

.enquiry-status {
  min-height: 20px;
  margin: 18px 0 0;
  color: #d4b47d;
  font-family: var(--serif);
  font-size: 16px;
  line-height: 1.15;
}

.enquiry-status:empty {
  display: none;
}

/* The left-hand enquiry desk is intentionally compact above the phone layout.
   Two-column fields remain, but their vertical rhythm no longer makes the form
   read as a second full page inside the office section. */
@media (min-width: 681px) {
  .enquiry-form__heading { padding-bottom: 24px; }
  .enquiry-form__heading > p { margin-bottom: 14px; }
  .enquiry-form__heading > span { margin-top: 14px; }
  .enquiry-fields { column-gap: 24px; }
  .enquiry-field { min-height: 88px; padding-top: 18px; }
  .enquiry-field--message { min-height: 144px; }
  .enquiry-field textarea { min-height: 80px; }
  .enquiry-form__submit { padding-top: 12px; }
}

/* THE CONFIRMATION DIALOG'S RULES ARE GONE WITH IT. Success is said in the
   page now — the live region in words, and the seal struck into the submit row
   by enquiry-seal.js — so there is no modal to style, nothing to dismiss, and
   nothing covering the animation that was already drawing underneath it. */


/* ------------------------------------------------------------
   Map
   ------------------------------------------------------------ */

/* The single most off-brand element on the route: a stock Google embed, bright
   white inside a dark section, carrying its own pins, its own type and its own
   colour. It cannot be restyled through the iframe, so it is corrected
   optically — inverted and hue-rotated into a dark map, desaturated, then
   warmed back toward the route's palette by an overlay. The result reads as a
   plan drawn in the site's own ink rather than as somebody else's product
   pasted into the page. Interaction is untouched. */
/* THE LONG MAP. It is a grid item in a stretched row now, so its height is the
   height of the enquiry and the office facts together — no margin-top, because
   it no longer follows anything, and no fixed height, because the point is that
   it fills whatever the words beside it need. */
.office-map {
  min-height: clamp(215px, 18.2292vw, 350px);
  position: relative;
  overflow: hidden;
  border: 1px solid rgba(255, 255, 255, 0.16);
  background: #14151a;
  @media (max-width: 1180px) {
    min-height: 350px;
  }
}

.office-map iframe {
  filter: invert(0.92) hue-rotate(180deg) saturate(0.42) contrast(0.9) brightness(1.04);
}

/* Warms the neutral result back into the palette. `pointer-events: none` so the
   map underneath stays draggable. */
.office-map::after {
  content: "";
  position: absolute;
  z-index: 2;
  inset: 0;
  pointer-events: none;
  mix-blend-mode: soft-light;
  background: linear-gradient(152deg, rgba(150, 108, 154, 0.5), rgba(168, 104, 66, 0.34) 58%, rgba(70, 84, 130, 0.4));
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.16);
}

.office-map__fallback {
  position: absolute;
  z-index: 0;
  inset: 0;
  overflow: hidden;
  background:
    linear-gradient(31deg, transparent 0 42%, rgba(124, 115, 101, 0.18) 42.2% 42.7%, transparent 42.9% 100%),
    linear-gradient(102deg, transparent 0 64%, rgba(124, 115, 101, 0.14) 64.2% 64.65%, transparent 64.9% 100%),
    repeating-linear-gradient(7deg, transparent 0 44px, rgba(112, 106, 96, 0.1) 45px 46px),
    repeating-linear-gradient(96deg, transparent 0 66px, rgba(112, 106, 96, 0.08) 67px 68px);
}

.office-map__fallback i {
  width: 145%;
  height: 10px;
  position: absolute;
  left: -20%;
  top: 34%;
  border-block: 1px solid rgba(255, 255, 255, 0.4);
  background: rgba(190, 184, 171, 0.32);
  transform: rotate(-14deg);
}

.office-map__fallback i:nth-child(2) {
  width: 120%;
  top: 63%;
  left: -8%;
  transform: rotate(21deg);
}

.office-map__fallback i:nth-child(3) {
  width: 95%;
  top: 50%;
  left: 37%;
  transform: rotate(84deg);
}

.office-map__fallback span {
  width: 12px;
  height: 12px;
  position: absolute;
  z-index: 2;
  left: 55%;
  top: 48%;
  border: 3px solid #f5e6c6;
  border-radius: 50%;
  background: #9a5a3d;
  box-shadow: 0 0 0 8px rgba(154, 90, 61, 0.13);
}

.office-map iframe {
  width: 100%;
  height: 100%;
  min-height: clamp(215px, 18.2292vw, 350px);
  position: relative;
  z-index: 1;
  display: block;
  border: 0;
  opacity: 0.86;
  @media (max-width: 1180px) {
    min-height: 350px;
  }
}

/* Deleted: a second `.office-map::after`, declared 70 lines below the first and
   silently replacing it, so the documented soft-light grade never ran and the
   map got a flat grey haze instead. Its 1px frame is folded into the grade. */

.office-map > a {
  min-height: 50px;
  position: absolute;
  z-index: 2;
  right: 14px;
  bottom: 14px;
  padding: 13px 16px;
  display: inline-flex;
  align-items: center;
  gap: 20px;
  color: #f3eee5;
  background: rgba(29, 29, 26, 0.94);
  box-shadow: 0 10px 28px rgba(18, 17, 15, 0.16);
  font-size: clamp(8px, 0.4762vw, 14.4px);
  font-weight: 700;
  letter-spacing: 0.15em;
  text-transform: uppercase;
}

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

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

  .contact-hero__inner {
    grid-template-columns: minmax(0, 1fr) minmax(380px, 0.8fr);
    gap: clamp(30px, 2.5vw, 48px);
    @media (max-width: 1180px) {
      gap: 48px;
    }
}

  .contact-orbit {
    min-height: clamp(301px, 25.5208vw, 490px);
    @media (max-width: 1180px) {
      min-height: 490px;
    }
}


  .office-layout {
    grid-template-columns: minmax(430px, 1.08fr) minmax(300px, 0.92fr);
    gap: clamp(36px, 3.02083vw, 58px);
    @media (max-width: 1180px) {
      gap: 58px;
    }
}
}

@media (max-width: 960px) {
  .contact-hero {
    min-height: 680px;
  }

  /* The copy takes a larger share of a narrower frame, so the clear half gives
     ground and every mask stop moves right. The ramp survives; it starts later. */
  .contact-hero__frost span:nth-child(1) {
    mask-image: linear-gradient(90deg, #000 0%, #000 60%, transparent 86%);
    -webkit-mask-image: linear-gradient(90deg, #000 0%, #000 60%, transparent 86%);
  }

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

  .contact-hero__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%);
  }

  .contact-hero__inner {
    grid-template-columns: minmax(0, 1fr) minmax(320px, 0.76fr);
    gap: 34px;
  }

  .contact-hero h1 {
    font-size: clamp(84px, 12vw, 116px);
  }

  .contact-hero__statement {
    font-size: 22px;
  }

  .contact-orbit {
    min-height: 440px;
  }

  .contact-orbit__caption {
    width: calc(100% - 54px);
    right: 27px;
    bottom: 27px;
  }

  .contact-orbit__coordinate {
    top: 30px;
    left: 30px;
  }


  /* One track below 960: the left column's field grid needs the whole width,
     and a map beside it would be a 300px sliver. The map keeps its own row and
     a height that reads as a plan rather than a strip. */
  .office-layout {
    grid-template-columns: 1fr;
    gap: 46px;
  }

  .office-column {
    gap: 40px;
  }

  .office-map {
    min-height: 380px;
  }

  .office-map iframe {
    min-height: 380px;
  }

  /* The form used to follow the details here and carried a rule to separate
     itself from them. It is first in the section now, directly under the
     section heading's own rule — a second one 40px below it read as a doubled
     line, which is the ghosting the client objected to elsewhere. */
}

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

  .contact-hero {
    min-height: auto;
    padding-top: calc(var(--header-height) + 54px);
    padding-bottom: 72px;
  }

  /* The column rule has no column to divide once the grid stacks. */
  .contact-hero__settle::before {
    display: none;
  }

  /* A left-frosted / right-clear ramp is a wide-frame idea and does not survive
     a phone: the copy runs the full measure, so a clear right side would be
     sharp painting underneath text. The whole hero is frosted here instead,
     which costs nothing that matters — frosting defocuses colour, it does not
     remove it. Only the middle level is kept: the heaviest reads as flat colour
     at this width and the lightest leaves detail under the copy. */
  .contact-hero__frost span:nth-child(1),
  .contact-hero__frost span:nth-child(3) {
    display: none;
  }

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

  /* A phone sees a narrow slice of the field, and centred that slice is its calm
     half — which is how a colour-first build renders a grey screen on the device
     most people will open it on. The window moves onto the warm mass.
     That mass is on the *right*: sampled, the artwork is flat at 0.13 saturation
     across its left 60% and peaks at 0.27 at 92%. The old value said 16% and the
     comment explained it by a `scaleX(-1)` mirror that does not exist anywhere in
     this file, so the phone hero cropped to the blank half and rendered exactly
     the grey screen the rule was written to prevent.
     Set on the hero so the sharp field and all blurred copies move together. */
  .contact-hero {
    --field-focus: 88% center;
  }

  .contact-hero__frost::after {
    background: linear-gradient(180deg, rgba(246, 243, 235, 0.36), rgba(246, 243, 235, 0.24) 62%, rgba(246, 243, 235, 0.1));
  }

  .contact-hero__inner {
    min-height: 0;
    grid-template-columns: 1fr;
    gap: 50px;
  }

  .contact-kicker {
    margin-bottom: 27px;
  }

  .contact-hero h1 {
    font-size: clamp(76px, 24vw, 98px);
    line-height: 0.86;
  }

  .contact-hero__statement {
    max-width: 520px;
    margin-top: 37px;
    font-size: clamp(21px, 5.9vw, 25px);
    line-height: 1.15;
  }

  .contact-hero__actions {
    margin-top: 32px;
    align-items: stretch;
    flex-direction: column;
    gap: 13px;
  }

  .contact-primary-action {
    width: 100%;
  }

  .contact-text-action {
    width: fit-content;
  }

  .contact-orbit {
    min-height: 322px;
  }

  .contact-orbit::before {
    inset: 12px;
  }

  .contact-orbit__coordinate {
    top: 27px;
    left: 26px;
  }

  .contact-orbit__sphere {
    width: 122%;
    right: -33%;
  }

  .contact-orbit__caption {
    width: calc(100% - 48px);
    right: 24px;
    bottom: 22px;
    align-items: flex-start;
    flex-direction: column;
    gap: 8px;
    padding-top: 13px;
  }

  .contact-orbit__caption strong {
    font-size: 18px;
  }

  .testimonials-section {
    padding-top: 22px;
    padding-bottom: 90px;
  }

  .office-section {
    padding-top: 80px;
    padding-bottom: 90px;
  }

  .contact-section-heading {
    /* No padding override: 19px under a 30px heading is 0.63em, where the rest of the
       page holds 0.39. The clamp's 12px floor is that ratio. */
    grid-template-columns: 1fr;
    align-items: start;
    gap: 15px;
  }

  .contact-section-heading h2 {
    /* No size override. It read clamp(40px, 11vw, 49px) — larger than the new desktop
       cap, so a phone would have been given a bigger section heading than a 2560px
       screen. The clamp's own 30px floor holds the ratio. */
    line-height: 1.02;
  }

  .contact-section-link {
    width: 100%;
    justify-content: space-between;
  }








  /* `display: contents` and three `order` declarations used to run the phone
     order details -> form -> map. They are gone: the DOM order is enquiry ->
     facts -> map at every other width, and reordering only here meant a
     keyboard user tabbed into the form before reaching links that were drawn
     above it. Somebody who only wants to ring the office still has "Call (02)
     9249 9162" in the hero, above the fold, on the same screen. */
  .office-layout {
    padding-top: 42px;
    display: flex;
    flex-direction: column;
    gap: 54px;
  }

  .office-column {
    gap: 38px;
  }

  .office-map {
    /* An auto-width map in a column flexbox shrink-wraps to the iframe's 300px
       intrinsic width while its siblings fill the 350px phone column. */
    width: 100%;
    min-height: 340px;
  }

  .office-map iframe {
    min-height: 340px;
  }

  .office-detail dd {
    font-size: clamp(20px, 5.8vw, 24px);
  }

  /* No override here either, and this one was the plainest statement of the fault: at
     51px against the section heading's 49px, the form's heading was *larger than the
     section that contains it* on every phone. */

  .enquiry-fields {
    grid-template-columns: 1fr;
  }

  .enquiry-field--message {
    grid-column: auto;
  }

  .enquiry-form__submit {
    align-items: flex-start;
    flex-direction: column;
    gap: 8px;
  }

  .enquiry-form__submit button {
    width: min(220px, 100%);
  }

  .enquiry-form__submit > p {
    width: min(300px, calc(100% - 62px));
    max-width: none;
  }

  /* This is the form's standalone legal action, not a link embedded in reading copy.
     Grow its hit area into the line box without changing the note's visible rhythm. */
  .enquiry-form__submit > p a {
    display: inline-block;
    padding-block: 4px;
    margin-block: -4px;
  }

  .office-map > a {
    right: 10px;
    bottom: 10px;
  }
}

/* ------------------------------------------------------------
   Landfall — the Contact route's interactive piece

   Built from the route's own material: the paper ground, the pigment palette,
   hairlines and the copper accent. No new colours, no new shapes. The map is
   the same projection as the hero globe, so this reads as the page continuing
   rather than as a widget bolted on.
   ------------------------------------------------------------ */

.puzzle {
  padding: clamp(96px, 6.77083vw, 130px) var(--contact-gutter) clamp(104px, 7.29167vw, 140px);
  position: relative;
  isolation: isolate;
  color: var(--contact-ink);
  background-color: #f1eee6;
  background-image: var(--contact-grain);
  background-size: 190px 190px;
  background-blend-mode: soft-light;
  @media (max-width: 1180px) {
    padding: clamp(96px, 8vw, 130px) var(--contact-gutter) clamp(104px, 8.6vw, 140px);
  }
}

.puzzle::before {
  content: "";
  position: absolute;
  z-index: -1;
  inset: 0;
  pointer-events: none;
  background:
    radial-gradient(1300px 520px at 88% 2%, rgba(120, 94, 116, 0.13), transparent 70%),
    radial-gradient(1000px 420px at 6% 100%, rgba(150, 118, 80, 0.1), transparent 68%);
  mask-image: linear-gradient(180deg, transparent 0, #000 170px, #000 calc(100% - 130px), transparent 100%);
  -webkit-mask-image: linear-gradient(180deg, transparent 0, #000 170px, #000 calc(100% - 130px), transparent 100%);
}

/* One instrument column and one map column. The live dashboard, curve and leaderboard
   stay together on the left; the Australia stage owns the larger right-hand field. The
   source order still lets a phone read title -> map -> dashboard -> standings. */
.puzzle__inner {
  max-width: 1480px;
  margin: 0 auto;
  display: grid;
  grid-template-columns: minmax(300px, 0.76fr) minmax(520px, 1.34fr);
  grid-template-areas:
    "head      head"
    "brief     stage"
    "standings stage";
  column-gap: clamp(52px, 5.2vw, 100px);
  row-gap: clamp(30px, 2.5vw, 44px);
  align-items: start;
  @media (max-width: 1180px) {
    column-gap: clamp(42px, 4.4vw, 68px);
    row-gap: clamp(30px, 3.2vw, 44px);
  }
}

.puzzle__head { grid-area: head; }
.puzzle__brief { grid-area: brief; }
.puzzle__standings { grid-area: standings; }

/* The stage owns the wide right-hand column. */
.puzzle__stage { grid-area: stage; }

/* The run in progress. A hairline over the top gives the column a head, which a bare
   stack of four lines in a 300px track does not have. */
.puzzle__brief {
  padding: 18px 0 22px;
  border-top: 1px solid rgba(92, 49, 34, 0.22);
  border-bottom: 1px solid rgba(92, 49, 34, 0.12);
}

.puzzle__panel-label {
  margin: 0 0 18px;
  color: var(--contact-gold, #a07b45);
  font-family: var(--sans);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.18em;
  text-transform: uppercase;
}

/* Both marks ship in the markup and the heading's class picks one, so the icon cannot
   drift out of step with the label it sits beside. */
/* Both marks are always drawn; the container's class decides which one is lit. Showing
   only the active one would say what board you are on and not that there is another. */
.puzzle__boards {
  display: flex;
  align-items: center;
  gap: 9px;
  margin: 0 0 11px;
  color: var(--contact-gold, #a07b45);
}

.puzzle__boards .puzzle__board-mark { opacity: 0.2; }
.puzzle__boards.is-pointer .puzzle__board-mark--pointer,
.puzzle__boards.is-touch .puzzle__board-mark--touch { opacity: 1; }

.puzzle__icon,
.puzzle__board-mark {
  width: 14px;
  height: 14px;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.puzzle__board-mark {
  width: 17px;
  height: 17px;
  stroke-width: 1.5;
  transition: opacity 260ms ease;
}

/* A replaced element's baseline is its bottom edge, so beside a thirty-pixel numeral the
   clock mark would sit on the floor. Aligned to the digits instead. */
.puzzle__clock .puzzle__icon {
  align-self: center;
  position: relative;
  top: 1px;
  opacity: 0.75;
}

.puzzle__kicker {
  margin-bottom: clamp(20px, 2.4vw, 30px);
}

/* A section heading like the other two, so it is sized like them. It was on its own
   third scale — 46px at 1440 where the others were 65 — which is why this page had no
   consistent idea of what a section heading is. */
.puzzle h2 {
  margin: 0;
  font-family: var(--serif);
  font-size: clamp(30px, 2.29167vw, 44px);
  font-weight: 400;
  letter-spacing: -0.028em;
  line-height: 1.04;
  @media (max-width: 1180px) {
    font-size: clamp(30px, 2.9vw, 44px);
  }
}

.puzzle__lede {
  max-width: 34ch;
  margin: clamp(16px, 1.6vw, 22px) 0 0;
  color: #4a4842;
  font-family: var(--serif);
  font-size: clamp(16px, 1.2vw, 19px);
  line-height: 1.15;
}

/* The running commentary. It carries the game's state for anyone not looking at
   the map, which is why it is a live region and not decoration. */
/* Not uppercase any more. Caps are a label treatment and this is a sentence: at eleven
   pixels, bold, tracked out to 0.14em, "Sydney. Level 12, 20 Hunter Street - that is us."
   ran to two shouted lines. It reads as a line of text now, which is what it is. */
.puzzle__status {
  min-height: 2.6em;
  margin: 0 0 2px;
  color: #5c3122;
  font-family: var(--sans);
  font-size: 12.5px;
  font-weight: 500;
  letter-spacing: 0.005em;
  line-height: 1.15;
}

/* The clock. Tabular figures because a proportional 1 is narrower than a 0, and a
   hundredths digit changing sixty times a second under proportional figures makes the
   whole line twitch. */
.puzzle__clock {
  display: flex;
  align-items: baseline;
  gap: 12px;
  margin: 14px 0 0;
  color: #5c3122;
  font-family: var(--sans);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}

.puzzle__time {
  font-size: clamp(16px, 1.35417vw, 26px);
  font-weight: 400;
  letter-spacing: -0.02em;
  line-height: 1;
  @media (max-width: 1180px) {
    font-size: 26px;
  }
}

.puzzle__time i {
  margin-left: 2px;
  font-size: 13px;
  font-style: normal;
  opacity: 0.55;
}

.puzzle__best {
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  opacity: 0.72;
}

.puzzle__best b {
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

/* A record just set. One pulse of the site's gold, then it stays. */
.puzzle__best.is-new {
  color: var(--contact-gold, #a07b45);
  opacity: 1;
  animation: puzzle-best-in 620ms ease-out;
}

@keyframes puzzle-best-in {
  0% { transform: translateY(3px); opacity: 0; }
  100% { transform: translateY(0); opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .puzzle__best.is-new { animation: none; }
}

/* Where the run landed among everyone else's. Appears only once there is a result to
   report; an empty line reserving space is worse than the line arriving. */
.puzzle__rank {
  margin: 7px 0 0;
  color: rgba(92, 49, 34, 0.72);
  font-family: var(--sans);
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.005em;
  line-height: 1.15;
  font-variant-numeric: tabular-nums;
}

.puzzle__rank b {
  color: var(--contact-gold, #a07b45);
  font-weight: 700;
}



/* Signing a qualifying run now happens in a real prompt. The previous input was a faint
   underline inside the dashboard; this surface gives the request a reason, a visible
   label, and an obvious way to decline. */
.puzzle-name-dialog {
  width: min(calc(100% - 32px), 560px);
  max-width: none;
  max-height: calc(100dvh - 32px);
  margin: auto;
  padding: 0;
  border: 0;
  overflow: auto;
  color: var(--contact-ink, #211f1c);
  background: transparent;
}

.puzzle-name-dialog::backdrop {
  background: rgba(22, 20, 18, 0.52);
  -webkit-backdrop-filter: blur(8px) saturate(0.86);
  backdrop-filter: blur(8px) saturate(0.86);
}

.puzzle-name-dialog[open] {
  animation: puzzle-name-in 360ms cubic-bezier(0.22, 1, 0.36, 1);
}

.puzzle-name-dialog__surface {
  position: relative;
  padding: clamp(30px, 3.2vw, 46px);
  overflow: hidden;
  border: 1px solid rgba(160, 123, 69, 0.34);
  border-radius: 4px;
  background:
    linear-gradient(145deg, rgba(255, 255, 255, 0.9), rgba(242, 235, 226, 0.94)),
    var(--contact-paper, #f4f1ea);
  box-shadow: 0 28px 90px rgba(22, 17, 13, 0.26);
}

.puzzle-name-dialog__surface::before {
  content: "";
  position: absolute;
  inset: 0 0 auto;
  height: 3px;
  background: linear-gradient(90deg, #a07b45, #d5b47d 62%, transparent);
}

.puzzle-name-dialog__close {
  width: 42px;
  height: 42px;
  position: absolute;
  z-index: 1;
  top: 13px;
  right: 13px;
  border: 0;
  color: rgba(33, 31, 28, 0.72);
  background: transparent;
  cursor: pointer;
  font: 300 28px/1 var(--sans);
}

.puzzle-name-dialog__close:focus-visible {
  outline: 2px solid var(--contact-gold, #a07b45);
  outline-offset: 0;
}

.puzzle-name-dialog__eyebrow {
  margin: 0 48px 13px 0;
  color: var(--contact-gold, #a07b45);
  font-family: var(--sans);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.18em;
  text-transform: uppercase;
}

.puzzle-name-dialog h3 {
  max-width: 12ch;
  margin: 0;
  font-family: var(--serif);
  font-size: clamp(31px, 3vw, 44px);
  font-weight: 400;
  letter-spacing: -0.035em;
  line-height: 0.98;
}

.puzzle-name-dialog__surface > p:last-of-type {
  max-width: 45ch;
  margin: 17px 0 0;
  color: rgba(33, 31, 28, 0.7);
  font-family: var(--sans);
  font-size: 14px;
  line-height: 1.42;
}

.puzzle__sign {
  margin: 26px 0 0;
  display: grid;
  gap: 10px;
}

.puzzle__sign label {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  color: var(--contact-ink, #211f1c);
  font-family: var(--sans);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.puzzle__sign label span {
  color: rgba(33, 31, 28, 0.52);
  font-size: 9px;
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: none;
}

.puzzle__sign input {
  width: 100%;
  min-height: 54px;
  padding: 0 15px;
  border: 1px solid rgba(92, 49, 34, 0.28);
  border-radius: 2px;
  color: var(--contact-ink);
  background: rgba(255, 255, 255, 0.58);
  font-family: var(--sans);
  font-size: 16px;
  letter-spacing: 0;
}

.puzzle__sign input::placeholder {
  color: rgba(92, 49, 34, 0.52);
}

.puzzle__sign input:focus-visible {
  outline: 2px solid rgba(160, 123, 69, 0.38);
  outline-offset: 1px;
  border-color: var(--contact-gold, #a07b45);
}

.puzzle__sign-actions {
  margin-top: 8px;
  display: flex;
  justify-content: flex-end;
  gap: 10px;
}

.puzzle__sign-actions button {
  min-height: 46px;
  padding: 0 18px;
  border: 1px solid rgba(33, 31, 28, 0.28);
  border-radius: 2px;
  cursor: pointer;
  font-family: var(--sans);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  transition: color 220ms ease, background-color 220ms ease, border-color 220ms ease;
}

.puzzle__sign-skip {
  color: rgba(33, 31, 28, 0.72);
  background: transparent;
}

.puzzle__sign-save {
  border-color: #211f1c !important;
  color: #f6f1e9;
  background: #211f1c;
}

.puzzle__sign-skip:hover,
.puzzle__sign-skip:focus-visible { border-color: var(--contact-ink); color: var(--contact-ink); }

.puzzle__sign-save:hover,
.puzzle__sign-save:focus-visible { border-color: #5c3122 !important; background: #5c3122; }

@keyframes puzzle-name-in {
  from { opacity: 0; transform: translateY(10px) scale(0.985); }
  to { opacity: 1; transform: translateY(0) scale(1); }
}

/* The field, as a curve.
 *
 * preserveAspectRatio="none" so the path stretches to whatever width the column has —
 * the shape is what carries the meaning, not any particular slope. */
.puzzle__curve {
  margin: 24px 0 0;
  max-width: clamp(209px, 17.7083vw, 340px);
  @media (max-width: 1180px) {
    max-width: 340px;
  }
}

.puzzle__curve svg {
  width: 100%;
  height: 92px;
  display: block;
  overflow: visible;
}

.puzzle__curve-fill { fill: rgba(92, 49, 34, 0.09); }

.puzzle__curve-line {
  fill: none;
  stroke: rgba(92, 49, 34, 0.45);
  stroke-width: 1;
  /* The path is drawn in a stretched coordinate system, so a plain stroke would come
     out thicker vertically than horizontally. This keeps it even. */
  vector-effect: non-scaling-stroke;
  stroke-linejoin: round;
}

.puzzle__curve-mark {
  stroke: var(--contact-gold, #a07b45);
  stroke-width: 1.5;
  vector-effect: non-scaling-stroke;
}

.puzzle__curve figcaption {
  position: relative;
  height: 14px;
  margin-top: 2px;
  font-family: var(--sans);
  font-size: clamp(10.5px, 0.625vw, 18.9px);
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--contact-gold, #a07b45);
}

/* Positioned from the left by the script and pulled back by half its own width, so the
   label sits under the line rather than starting at it. */
.puzzle__curve figcaption span {
  position: absolute;
  white-space: nowrap;
}

/* The board. Tabular figures and a fixed rank column, so ten rows read as a column of
   times rather than ten separate lines that happen to be underneath each other. */
.puzzle__ranking {
  margin: 26px 0 0;
  max-width: clamp(209px, 17.7083vw, 340px);
  @media (max-width: 1180px) {
    max-width: 340px;
  }
}

.puzzle__ranking h3 {
  margin: 0 0 10px;
  color: #5c3122;
  font-family: var(--sans);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
}

.puzzle__ranking ol {
  margin: 0;
  padding: 0;
  list-style: none;
  border-top: 1px solid rgba(92, 49, 34, 0.16);
}

.puzzle__ranking li {
  display: grid;
  grid-template-columns: 20px auto minmax(0, 1fr);
  align-items: baseline;
  gap: 12px;
  padding: 5px 0;
  border-bottom: 1px solid rgba(92, 49, 34, 0.1);
  color: var(--contact-ink);
  font-family: var(--sans);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
}

.puzzle__ranking li i {
  color: rgba(92, 49, 34, 0.6);
  font-style: normal;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.08em;
}

.puzzle__ranking li span {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* A place nobody signed simply ends after the time. The dash that used to stand in for
   the name turned ten unsigned rows into a column of what looked like missing data. */
.puzzle__ranking li span {
  color: rgba(92, 49, 34, 0.62);
  font-size: 12px;
}

.puzzle__ranking li b {
  font-weight: 400;
  letter-spacing: -0.01em;
}

/* The run just finished. */
.puzzle__ranking li.is-mine {
  color: var(--contact-gold, #a07b45);
}

/* Why the board a visitor is looking at is not the board their colleague described.
   Quieter than the times it explains — it is a footnote, and it earns its place only
   because two people comparing a phone with a laptop would otherwise think one of them
   was reading a broken page.

   It was the same pigment at 0.5 alpha: 2.62:1 against the puzzle paper, the lowest
   text contrast on the site. At 0.78 it remains clearly subordinate to the solid-ink
   times while staying above 4.5:1 even against --contact-paper-deep. */
.puzzle__ranking-note {
  margin: 8px 0 0;
  color: rgba(92, 49, 34, 0.78);
  font-family: var(--sans);
  font-size: 12px;
  line-height: 1.15;
}

/* THE BUTTON, and it is a member of a family rather than an invention.
   `.contact-primary-action` — "Start an enquiry" on this same page — is a
   rectangle at 218x44 that is filled at rest and inverts to an outline on hover.
   This is the same rectangle, the same metrics and the same type, starting as
   the outline and filling instead: one rank quieter, which is what "start again"
   is next to "start an enquiry", and recognisably the same object.

   WHAT IT REPLACED had eight parts for one action — a dashed orbit ring, a
   filled disc, an arrow, an eyebrow reading "RESET THE BOARD", a serif label
   reading "Start again", a floating "01" that indexed nothing (there was no 02),
   a gold rail and two rules — inside 340x82. The client's word was 臃肿. Two
   visible parts now, a label and an arrow, inside 218x44.

   THE ANIMATION IS THE FILL ARRIVING, not a colour switching. It is scaled from
   the left edge over 520ms, so what the eye reads is a movement across the
   button rather than a state change on it, and the arrow steps 4px in the same
   direction as it goes. Both are transforms on a composited layer: no layout, no
   paint of the text. */
.puzzle__reset {
  min-width: 218px;
  min-height: 44px;
  margin-top: 28px;
  padding: 11px 14px 11px 17px;
  position: relative;
  isolation: isolate;
  overflow: hidden;
  display: inline-flex;
  align-items: center;
  justify-content: space-between;
  gap: clamp(20px, 1.66667vw, 32px);
  border: 1px solid var(--contact-ink);
  border-radius: 0;
  color: var(--contact-ink);
  background: transparent;
  box-shadow: none;
  cursor: pointer;
  text-align: left;
  font-family: var(--sans);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.13em;
  line-height: 1;
  text-transform: uppercase;
  transition: color 260ms ease;
}

/* The same ink, grain and gradient the filled primary uses, so the two buttons
   are the same object seen in two states rather than two different browns. */
.puzzle__reset-fill {
  position: absolute;
  z-index: -1;
  inset: 0;
  pointer-events: none;
  background-color: var(--contact-ink);
  background-image: var(--contact-grain), linear-gradient(168deg, #2b2b28 0%, #1d1d1a 52%, #121210 100%);
  background-size: 140px 140px, auto;
  background-blend-mode: soft-light, normal;
  transform: scaleX(0);
  /* THE LIGHT PASSES THROUGH, it does not retreat. The origin is the RIGHT edge
     at rest and the LEFT edge while hovered, and switching between them is free
     at both moments it happens: on entry the fill is at scaleX(0) and invisible,
     on exit it is at scaleX(1), where the painted box is identical whatever the
     origin. So the fill grows in from the left and, when the pointer leaves,
     keeps going and collapses out through the right. No JavaScript, and no frame
     where anything jumps. */
  transform-origin: 100% 50%;
  transition: transform 520ms cubic-bezier(0.22, 1, 0.36, 1);
}

/* One gold line, and the page's own gold. The button is otherwise ink on paper,
   which is correct and was also the whole of the client's "枯燥". It travels with
   the fill and by the same rule. */
.puzzle__reset::after {
  content: "";
  position: absolute;
  z-index: 1;
  right: 0;
  bottom: 0;
  left: 0;
  height: 1px;
  pointer-events: none;
  background: rgba(160, 123, 69, 0.95);
  transform: scaleX(0);
  transform-origin: 100% 50%;
  transition: transform 520ms cubic-bezier(0.22, 1, 0.36, 1);
}

.puzzle__reset-arrow {
  width: 18px;
  height: 18px;
  flex: 0 0 auto;
  fill: none;
  stroke: currentColor;
  stroke-linecap: square;
  stroke-linejoin: miter;
  stroke-width: 1.35;
  transition: transform 380ms cubic-bezier(0.22, 1, 0.36, 1);
}

/* IT TURNS, because that is what the button does. `rotate` rather than a
   transform function, so the press spin and the hover's 4px step compose instead
   of overwriting each other — and so GSAP never has to own this element's
   transform. */
@keyframes puzzle-reset-turn {
  from { rotate: 0deg; }
  to { rotate: 360deg; }
}

.puzzle__reset.is-pressed .puzzle__reset-arrow {
  animation: puzzle-reset-turn 620ms cubic-bezier(0.65, 0, 0.35, 1);
}

/* The word carries the state of the game, so it changes; it arrives from just
   below rather than switching, which is the difference between a label and a
   readout. */
@keyframes puzzle-reset-word {
  from { opacity: 0; transform: translateY(4px); }
}

.puzzle__reset-label {
  min-width: 0;
  display: inline-block;
}

.puzzle__reset-label.is-changed {
  animation: puzzle-reset-word 220ms cubic-bezier(0.22, 1, 0.36, 1);
}

.puzzle__reset:hover,
.puzzle__reset:focus-visible {
  color: var(--contact-paper);
}

.puzzle__reset:hover .puzzle__reset-fill,
.puzzle__reset:focus-visible .puzzle__reset-fill,
.puzzle__reset:hover::after,
.puzzle__reset:focus-visible::after {
  transform: scaleX(1);
  transform-origin: 0 50%;
}

.puzzle__reset:hover .puzzle__reset-arrow,
.puzzle__reset:focus-visible .puzzle__reset-arrow {
  transform: translateX(4px);
}

.puzzle__reset:focus-visible {
  outline: 2px solid var(--contact-ink);
  outline-offset: 3px;
}


.puzzle__reset:focus-visible {
  outline: 2px solid var(--contact-gold, #a07b45);
  outline-offset: 4px;
}

/* The stage.
   Centred in its own track. It used to be `margin-left: auto`, which was right for the
   second of two columns and wrong the moment the grid changed — the same reason it had
   to be reset at 768 in the old layout. The aspect ratio matches the generated viewBox
   exactly, which is what lets a piece's offset be a percentage. */
.puzzle__stage {
  width: min(100%, 720px);
  margin-inline: 0;
  justify-self: end;
  position: relative;
  aspect-ratio: 1340 / 1204;
  touch-action: manipulation;
}

.puzzle__board,
.puzzle__marker {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  overflow: visible;
}

.puzzle__shape {
  width: 100%;
  height: 100%;
  display: block;
  overflow: visible;
}

/* The picture on the box. Without it the opening screen is seven shapes adrift with
   nothing to say where any of them goes. */
.puzzle__ghost {
  fill: rgba(120, 94, 116, 0.055);
  stroke: rgba(74, 60, 58, 0.26);
  stroke-width: 1.4px;
  stroke-dasharray: 5 6;
  vector-effect: non-scaling-stroke;
}

/* Only the shape inside takes the pointer — otherwise a rectangular box would swallow
   clicks meant for its neighbours. The button stays focusable, which is how the
   keyboard route works. */
/* Sized and placed by the script to its own shape's bounding rectangle, not to the
   whole stage — see contact-puzzle.js. */
.puzzle__piece {
  position: absolute;
  padding: 0;
  border: 0;
  background: none;
  pointer-events: none;
  transition: transform 420ms cubic-bezier(0.22, 1, 0.36, 1), opacity 360ms ease;
}

/* The island remains its geographically correct size while its phone target does not.
   This child exists only on Tasmania; it is far from the mainland, so the enlarged
   affordance cannot steal a neighbouring state. Pointer events bubble to the button,
   which hands capture to the SVG for the rest of the drag. */
.puzzle__piece-hit {
  width: 44px;
  height: 44px;
  position: absolute;
  z-index: 2;
  left: 50%;
  top: 50%;
  display: block;
  transform: translate(-50%, -50%);
  background: transparent;
  pointer-events: auto;
  touch-action: none;
  cursor: grab;
}

.puzzle__piece.is-dragging .puzzle__piece-hit,
.puzzle__piece.is-carrying .puzzle__piece-hit {
  cursor: grabbing;
}

.puzzle__piece.is-placed .puzzle__piece-hit {
  cursor: default;
}

.puzzle__fill {
  fill: #c4a173;
  stroke: #f1eee6;
  stroke-width: 1.6px;
  vector-effect: non-scaling-stroke;
  pointer-events: auto;
  cursor: grab;
  /* Without this a drag that begins on a piece scrolls the page on a touchscreen. */
  touch-action: none;
  transition: fill 260ms ease, filter 260ms ease;
  filter: drop-shadow(0 6px 12px rgba(60, 42, 26, 0.24));
}

/* The Australian Capital Territory, an enclave inside New South Wales, drawn as a
   line on that piece.
   Named __enclave and not __inner, which is what it was: the section's own layout
   wrapper is .puzzle__inner, so this rule was landing on the entire left-hand column
   and giving it `pointer-events: none`. The heading, the status line and the "Start
   again" button were all un-clickable, and nothing in the console said so. */
.puzzle__enclave {
  fill: none;
  stroke: rgba(93, 66, 38, 0.5);
  stroke-width: 1.1px;
  vector-effect: non-scaling-stroke;
  pointer-events: none;
}

.puzzle__piece:hover .puzzle__fill {
  fill: #cfae82;
}

/* Caught by the magnet. The piece brightens and lifts, and the outline it is being
   pulled into firms up out of its dashed state — so the pull is something you can see
   as well as feel. */
.puzzle__ghost.is-near {
  stroke: rgba(92, 49, 34, 0.5);
  stroke-dasharray: none;
  fill: rgba(160, 123, 69, 0.12);
}

.puzzle__piece.is-near .puzzle__fill {
  fill: #dcbd92;
  filter: drop-shadow(0 16px 30px rgba(60, 42, 26, 0.36));
}

/* Nothing eases while it is under the pointer: a piece that lags behind the cursor
   feels broken rather than smooth. */
.puzzle__piece.is-dragging {
  transition: none;
}

.puzzle__piece.is-dragging .puzzle__fill,
.puzzle__piece.is-carrying .puzzle__fill {
  cursor: grabbing;
  fill: #d8b98d;
  filter: drop-shadow(0 14px 26px rgba(60, 42, 26, 0.34));
}

.puzzle__piece.is-placed .puzzle__fill {
  fill: #a07b45;
  cursor: default;
  filter: none;
}

.puzzle__piece:focus-visible .puzzle__fill {
  stroke: #5c3122;
  stroke-width: 2.6px;
}

.puzzle__piece:focus-visible {
  outline: none;
}

/* Sydney's marker, which is the mark itself rather than a dot — see contact-puzzle.js.
   Hidden until the light's shrinking mark arrives on it, because until then there is
   nothing at this point to indicate: the mark landing is what puts it there. */
.puzzle__marker {
  pointer-events: none;
}

/* Instant, not a fade. This is a swap of one identical shape for another — the copy
   that animates is position: fixed and so cannot stay behind, and the map's own copy has
   to take over in the same frame. A 200ms fade-in against a 120ms removal left a visible
   dip between the two. */
.puzzle__sydney {
  opacity: 0;
}

/* Painted, but not visible. The whole marker is rasterised for the first time in
   whatever frame reveals it — outlines, clip path, and the band inside the clip — and
   that frame used to be the landing: a repeatable 65ms frame at iPad size, right where
   the landing beat needs every frame it has. This carries the cost out to the start of
   the rays instead. Four thousandths of an opacity over a gold map is nothing. */
.puzzle__sydney.is-warming {
  opacity: 0.004;
}

.puzzle__sydney.is-lit {
  opacity: 1;
}

/* Where the mark comes to rest. Dark, because Sydney is on the coast and the mark
   straddles the coastline: this is the one colour that reads against both the gold of
   New South Wales and the paper of the sea, without adding anything to the logo to prop
   it up. The animating copy interpolates to exactly this as it contracts. */
/* The mark itself. More golden than it was: at 18px tall rather than 13 it carries the
   lower contrast against the land (1.8:1) that this colour costs, and the sheen below
   gives it highlights the flat version never had. */
.puzzle__sydney-ink {
  fill: #7a5012;
}

/* The group the ink and the sheen hang from. It carries no motion of its own — see
   below — and is kept separate from the group carrying the position because a CSS
   transform replaces an element's transform attribute outright. */
.puzzle__sydney-settle {
  transform-box: fill-box;
  transform-origin: 50% 50%;
}

/* There is no rebound here any more, and that is the point.

   A scale beat on this element sits at the seam between two animation systems: the
   contraction is a JS transform on a fixed-position copy of the mark, and this would be
   CSS keyframes on the map's own. The two have to agree on a size and a velocity at the
   instant of the swap, and twice they did not. First the beat started at scale(0.82)
   while the contraction ended at 1.0, and the shape snapped 10.8% smaller in a single
   frame — 73.82px to 65.87px, at a steady 60fps. Starting it at 1.0 fixed the size and
   left the velocity: the contraction arrived travelling and the beat immediately
   reversed it outward, which is a jerk whether or not the numbers line up.

   Matching the velocities is not possible at a useful amplitude either — the arrival is
   fast enough that a continuous beat would have to be either 100ms long or 40% deep. So
   the seam is gone instead of tuned. The mark falls on one smooth curve and comes to
   rest, and the small animation on landing is the sheen crossing it, which is light
   rather than motion and cannot stutter. */

/* The sheen. A band of lighter gold crosses the mark, then waits a long time, so it
   reads as an occasional catch of light and never as a loading indicator.

   The band is what moves. This was a gradient with its gradientTransform animated, and
   WebKit never repainted the mark when that transform moved — the sweep worked in
   Chrome and was frozen on every iPhone and iPad. See contact-puzzle.js.

   It runs only under .is-lit, which is to say only once the mark has landed. The old
   version started a second after page load and repeated for the life of the page,
   invisible behind opacity: 0, keeping a paint alive on a page nobody had solved. */
.puzzle__sydney-band {
  transform: translateX(0);
}

/* The first sweep is the landing: 0.7s after the mark comes to rest, near enough to
   belong to it and far enough that the overlay is out of the document first. */
.puzzle__sydney.is-lit .puzzle__sydney-band {
  animation: yk-gleam 5.2s linear 0.7s infinite;
}

/* In mark units — 90 wide starting off the left edge, run past the right edge of a
   255-unit lockup, then hold there for the remaining 60% of the cycle. */
@keyframes yk-gleam {
  0%   { transform: translateX(0); }
  40%  { transform: translateX(400px); }
  100% { transform: translateX(400px); }
}

@media (prefers-reduced-motion: reduce) {
  .puzzle__sydney.is-lit .puzzle__sydney-band { animation: none; }
}



/* The light. Above everything on the page — the header sits at 100 and the search
   overlay at 1000 — and never in the way of a pointer. */
.puzzle__light {
  position: fixed;
  inset: 0;
  z-index: 1200;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

/* Three tracks stop working somewhere under 1024: the standings column reaches its 232px
   floor and the map is squeezed for the privilege. One column below that, ordered so the
   map comes straight after the instructions and the finished time lands directly under
   it — which is where you are looking when it appears. */
@media (max-width: 1024px) {
  .puzzle__inner {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas:
      "head"
      "stage"
      "brief"
      "standings";
    row-gap: 40px;
  }

  .puzzle__lede { max-width: 46ch; }
  .puzzle__stage { justify-self: center; }
  .puzzle__standings { max-width: 420px; }
}

@media (max-width: 560px) {
  .puzzle__sign label {
    display: grid;
    gap: 5px;
  }

  .puzzle__sign-actions {
    align-items: stretch;
    flex-direction: column-reverse;
  }

  .puzzle__sign-actions button { width: 100%; }
}

@media (max-width: 960px) {
  .puzzle__lede {
    max-width: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .puzzle__piece,
  .puzzle__fill,
  .puzzle__reset,
  .puzzle__reset-fill,
  .puzzle__reset-arrow,
  .puzzle__reset::after {
    transition: none;
  }

  .puzzle__reset.is-pressed .puzzle__reset-arrow,
  .puzzle__reset-label.is-changed {
    animation: none;
  }

  .puzzle-name-dialog[open] { animation: none; }
}

/* The spam trap. Removed from the layout entirely rather than hidden with
   `display: none`, which some automated submitters check for. */
.enquiry-trap {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* The mark the light resolves into.
   A real SVG above the canvas, not painted into it: a path element knows its own
   length, so drawing it on is the same dash arithmetic the homepage mark uses, and what
   appears here is identical to the logo at the top of the site. */
.puzzle__mark {
  position: fixed;
  /* Parked at the viewport's top left; the transform carries it to Sydney. Nothing in
     the frame loop writes left/top/width/height, so nothing in it invalidates layout. */
  left: 0;
  top: 0;
  z-index: 1201;
  pointer-events: none;
  overflow: visible;
  /* Scaled about its own middle, which is the point it is contracting onto. */
  transform-origin: 50% 50%;
  /* Transform only. `filter` was listed here and never used, and naming it makes the
     compositor keep a filter-capable layer for the life of the animation. */
  will-change: transform;
  /* The settle colour is written here, once a frame, and inherited by the outlines
     through currentColor — rather than the same string written onto each of them. This
     is where it starts: the solid is warmer than the line that draws it, so the
     brightest thing on screen is always the light doing the drawing, and the finished
     mark settles rather than glares. Near-white for both read as a sticker on the page. */
  color: #ffe9c6;
}

.puzzle__mark path {
  fill: currentColor;
  fill-opacity: 0;
  stroke: #fffaf0;
  stroke-width: 0;
  stroke-linejoin: round;
}


/* ============================================================
   The epigraph
   ------------------------------------------------------------
   Moved here from the People route on 2026-08-01, unchanged but for its name. The
   client took it off People — the homepage already opens on a maxim, and a firm
   that quotes itself twice on the way in reads as pleased with itself — and asked
   that it be kept rather than deleted. Contact is where it earns its place: the
   visitor is at the point of deciding whether to write, and the lines here are
   about reaching the other side rather than about people.

   Renamed from `people-maxim`, because a class that names a route it no longer
   sits on misleads whoever reads it next. Structure, cadence and the word-level
   reveal are exactly as they were.
   ============================================================ */

/* ------------------------------------------------------------
   Hero maxim
   ------------------------------------------------------------ */

/* The epigraph is a wide, shallow strip rather than a block of prose, and that
   shape is what stops it competing with the hero sentence — it reads as a
   colophon, which is what it is.

   It used to get its separation from a different ground: paper under it, colour
   above. At b59 the colour runs through it, so the separation is carried by a
   hairline above instead. That is the weaker signal of the two, but the strip
   shape was always doing most of the work. */
.epigraph-slot {
  position: relative;
  z-index: 3;
  /* --contact-gutter, not --gutter. The People route's token does not exist here, so
     the inherited declaration resolved to nothing and the epigraph sat flush against
     both edges of the window — the whole strip, on every width. A custom property that
     is not defined does not fall back, it drops the declaration.
     The vertical rhythm is deliberately tight. It was opened up when the hero above
     was 820px tall and the strip needed to read as a beat rather than a seam; with the
     hero cut to roughly 40% of that, the same padding just pushed the reviews back
     below the fold, which is the one thing this page must not do on a desktop. */
  padding: clamp(26px, 2.2vw, 38px) var(--contact-gutter) clamp(26px, 2.4vw, 40px);
  border-top: 1px solid rgba(46, 34, 40, 0.16);
}

.epigraph {
  margin: 0;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: start;
  column-gap: clamp(28px, 4.16667vw, 80px);
  @media (max-width: 1180px) {
    column-gap: clamp(28px, 4vw, 80px);
  }
}

.epigraph__quote {
  grid-column: 1;
  margin: 0;
}

.epigraph__en {
  margin: 0;
  color: #24211d;
  font-family: var(--serif);
  font-size: clamp(17px, 1.14583vw, 22px);
  font-weight: 400;
  letter-spacing: -0.008em;
  line-height: 1.15;
  @media (max-width: 1180px) {
    font-size: clamp(17px, 1.45vw, 22px);
  }
}

.epigraph[data-measure="long"] .epigraph__en {
  font-size: clamp(16px, 1.22vw, 19px);
  line-height: 1.15;
}

/* The source language, where it can be set safely. It is deliberately the
   quietest line in the block: provenance, not display. */
.epigraph__original {
  margin: clamp(8px, 0.8vw, 11px) 0 0;
  color: #7a746a;
  font-family: var(--serif);
  font-size: clamp(11.5px, 0.88vw, 13px);
  font-style: italic;
  letter-spacing: 0.008em;
  line-height: 1.15;
}

.epigraph__original[hidden] {
  display: none;
}

.epigraph__source {
  grid-column: 2;
  max-width: 44ch;
  margin: 4px 0 0;
  color: var(--label);
  font-family: var(--sans);
  font-size: clamp(9px, 0.5357vw, 16.2px);
  font-weight: 700;
  letter-spacing: 0.15em;
  line-height: 1.15;
  text-align: right;
  text-transform: uppercase;
}

/* The maxim keeps its word-level cadence, but only the final ink density
   develops. There is no travel or defocus: the quotation is already present
   and quietly becomes exact. */
.maxim-word {
  display: inline-block;
  opacity: 0.93;
  transition: opacity 480ms ease calc(var(--word-index, 0) * 24ms + 90ms);
  will-change: opacity;
}

.epigraph__original,
.epigraph__source {
  opacity: 0.95;
  transition: opacity 440ms ease calc(var(--word-index, 0) * 24ms + 90ms);
}

.epigraph.is-resolved .maxim-word,
.epigraph.is-resolved .epigraph__original,
.epigraph.is-resolved .epigraph__source {
  opacity: 1;
  will-change: auto;
}

@media (max-width: 680px) {
  .epigraph {
    grid-template-columns: 1fr;
    row-gap: 11px;
  }

  .epigraph__source {
    grid-column: 1;
    max-width: 100%;
    text-align: left;
  }

  .epigraph__en,
  .epigraph[data-measure="long"] .epigraph__en {
    font-size: 16px;
    line-height: 1.15;
  }

}



/* ============================================================
   The review band
   ------------------------------------------------------------
   Wide cards advancing one at a time without end, each showing the opening of a review and
   opening to the whole of it when clicked. It replaced a row of tall cards: eight
   portrait cards read as a table of contents, and the client asked for a banner — a
   long horizontal rectangle, not a stack of upright ones.

   reviews.js renders complete leading and trailing copies around the real set. The
   first trailing card is the final landing point, after which GSAP resets to its
   identical real card on the same frame.

   It stops while it is being read — on hover, on focus, and for as long as a card is
   open. A moving target cannot be clicked, and a card that sails away while its text
   is being read is worse than one that never opened.
   ============================================================ */

.review-band {
  position: relative;

  /* Full bleed. A band that stops inside the gutter announces its own ends, which is
     the one thing this composition must not do. */
  margin-inline: calc(var(--contact-gutter) * -1);
  overflow: hidden;
  /* Was 18/24 to clear a centred card scaled to 1.12 above its layout box. Nothing is
     scaled now, so this is ordinary breathing room around a row of equal cards. */
  padding: 10px 0 14px;
}

/* The fades are the band's ends. Unlike the scroller this replaced, they are always
   present: there is always more in both directions, and saying so is the point. */
.review-band::before,
.review-band::after {
  content: "";
  position: absolute;
  z-index: 2;
  top: 0;
  bottom: 0;
  width: clamp(48px, 8vw, 150px);
  pointer-events: none;
}

.review-band::before {
  left: 0;
  background: linear-gradient(90deg, var(--contact-paper, #f4f1ea) 18%, transparent 100%);
}

/* BOTH ENDS, NOW THAT IT DRIFTS. The right-hand wash used to be suppressed above
   1180px: with a rail that stepped and rested, a card was always parked in the middle
   and the eye never went to the edge, where the wash read as the loop stopping in a
   strip of paper. A drift has no centre — every card crosses the whole band and leaves
   by the left, so both edges are equally in view and both are where the movement comes
   from and goes to. Symmetrical fades are what make that read as endless rather than
   as cards being cut off by a box. */
.review-band::after {
  right: 0;
  background: linear-gradient(270deg, var(--contact-paper, #f4f1ea) 18%, transparent 100%);
}

/* The rail is draggable, so the horizontal axis belongs to it and the vertical axis
   stays with the page. Scoped away from `.is-manual`, which is a native horizontal
   scroller — pan-y there would take its own gesture away. */
.review-band:not(.is-manual) {
  touch-action: pan-y;
  cursor: grab;
}

.review-band.is-dragging {
  cursor: grabbing;
}

.review-band__rail {
  display: flex;
  align-items: flex-start;
  gap: clamp(16px, 1.35417vw, 26px);
  width: max-content;
  padding-inline: clamp(16px, 1.6vw, 26px);
  will-change: transform;
  @media (max-width: 1180px) {
    gap: clamp(16px, 1.6vw, 26px);
  }
}

.review-band.is-manual {
  overflow-x: auto;
  scrollbar-width: none;
}

.review-band.is-manual::-webkit-scrollbar { display: none; }

/* A banner: wide and shallow. The width is what makes three lines read as an opening
   rather than as a fragment — in the 380px column this replaced, three lines came to
   about twelve words.
 *
 * ONE BOX, ONE SIZE. `--review-card-height` is written onto the rail by reviews.js —
 * the tallest closed card, remeasured on resize and after the fonts settle. It is not a
 * number in this file because it is not a constant: the cards differ by whether the
 * reviewer's listing carried attribute tags and by whether their review reaches three
 * lines, and both change with the width of the screen. `auto` is the fallback for the
 * one frame before the measurement lands, and for a browser without the script. */
.review-card {
  flex: 0 0 auto;
  width: min(88vw, 540px);
  height: var(--review-card-height, auto);
}

/* The one card allowed out of the uniform box, because the reader asked it to be. */
.review-card.is-open {
  height: auto;
}

/* The focal scaling is gone with the stepping. It existed to say "this is the card
   being shown to you", and a rail that drifts is not showing you one card — it was
   also, together with content-sized heights, the reason no two cards on screen were
   ever the same size. Every card now sits at its own full size and full opacity. */

.review-card__toggle {
  width: 100%;
  height: 100%;
  margin: 0;
  padding: clamp(20px, 2vw, 28px) clamp(22px, 2vw, 30px) clamp(16px, 1.6vw, 22px);
  display: grid;
  /* The review takes the slack and the reviewer's name sits on the bottom edge, so
     the name line is level across every card in the band whatever the review's
     length. */
  grid-template-rows: 1fr auto;
  gap: clamp(14px, 1.4vw, 20px);
  border: 1px solid rgba(255, 255, 255, 0.5);
  border-radius: 3px;
  background:
    linear-gradient(135deg, rgba(255, 255, 255, 0.5), rgba(244, 236, 235, 0.26)),
    rgba(244, 241, 235, 0.28);
  box-shadow: none;
  -webkit-backdrop-filter: blur(16px) saturate(1.14);
  backdrop-filter: blur(16px) saturate(1.14);
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: border-color 300ms ease, background-color 300ms ease;
}

.review-card__toggle:hover {
  border-color: rgba(255, 255, 255, 0.72);
  background-color: rgba(255, 250, 245, 0.38);
}

.review-card__toggle:focus-visible {
  outline: 2px solid var(--contact-ink, #211f1c);
  outline-offset: 3px;
}

/* Collapsed to three *whole* lines. The lh unit follows the body's computed leading,
   including the larger phone leading below, so the boundary can never land halfway
   through a line. There is deliberately no fade mask: a naturally short one- or
   two-line review must remain completely readable. */
.review-card__body {
  display: grid;
  gap: 0.72em;
  /* `align-content: start` because the body now sits in a `1fr` row that is taller
     than three lines on most cards — the review begins at the top of the box, and
     the slack falls between it and the reviewer's name. */
  align-content: start;
  max-height: 3lh;
  overflow: hidden;
  color: var(--contact-ink);
  font-family: var(--serif);
  font-size: clamp(15px, 1.1vw, 17px);
  line-height: 1.15;
  transition: max-height 620ms cubic-bezier(0.22, 1, 0.36, 1);
}

.review-card__body p { margin: 0; }

/* The reviewer wrote in both languages; the site sets both, in the same relationship
   the hero and the epigraph use. */
.review-card__zh {
  color: var(--contact-muted, #6b6962);
  font-size: 0.86em;
  line-height: 1.15;
}

.review-card__meta {
  display: grid;
  gap: 8px;
}

/* Google's own attribute tags, set as quiet chips. Not a star row: the ratings render
   as icons on the listing and did not come through with the text, and an invented five
   stars on a law firm's site is a small lie with a large downside. */
.review-card__tags {
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 5px 6px;
  list-style: none;
}

.review-card__tags li {
  padding: 3px 7px;
  border: 1px solid rgba(160, 123, 69, 0.34);
  border-radius: 2px;
  color: var(--gold, #a07b45);
  font-family: var(--sans);
  font-size: clamp(9px, 0.5357vw, 16.2px);
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.review-card__line {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 12px;
}

.review-card__name {
  color: var(--contact-ink);
  font-family: var(--sans);
  font-size: clamp(11px, 0.6548vw, 19.8px);
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

.review-card__detail {
  color: var(--contact-muted, #6b6962);
  font-family: var(--sans);
  font-size: clamp(10px, 0.5952vw, 18px);
  letter-spacing: 0.1em;
}

/* Says what will happen, and changes once it has. */
.review-card__cue::after {
  content: "Read the full review";
  color: var(--gold, #a07b45);
  font-family: var(--sans);
  font-size: clamp(10px, 0.5952vw, 18px);
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

.review-card.is-open .review-card__cue::after {
  content: "Close";
}

@media (prefers-reduced-motion: reduce) {
  /* No automatic advance, and no second copy to scroll past. The
     band becomes a row that can be scrolled by hand, which is what it is underneath. */
  .review-band__rail { will-change: auto; }
  .review-band { overflow-x: auto; scrollbar-width: none; }
  .review-band::-webkit-scrollbar { display: none; }
  .review-card__body,
  .review-card__toggle { transition: none; }
}

/* TABLETS GET A REAL FLUID RANGE. The old `min(58vw, 430px)` hit its 430px cap
   before either common iPad width, so 768px and 820px rendered the same large
   card. The card now grows with the screen between explicit 320px and 390px
   bounds: compact at iPad portrait widths, but never phone-small or desktop-wide.
   Height remains content-measured by reviews.js, so the narrower card cannot
   clip a review or leave cards in one rail at mismatched heights. */
@media (min-width: 681px) and (max-width: 1024px) {
  .review-card {
    width: clamp(320px, 46vw, 390px);
  }
}

/* ONE RAMP FROM THE TABLET CEILING TO THE DESKTOP CARD, because the step that
   used to be here ended at 1180 and an iPad Pro in landscape is 1194 or 1366.
   Above the step the card took its full desktop 540px in a band 250px narrower
   than a laptop's, so the rail held ONE whole card where every other screen
   holds two and a half — measured 2.15 cards at 1194 against 2.54 at 1440.
   38vw is what keeps that ratio constant: 390px at 1025, which is exactly the
   ceiling of the tablet rule above so the two meet without a jump, 454 at 1194,
   519 at 1366, and the desktop 540 from 1421 up. The band shows 2.5 cards at
   every width in the range, which is what it was tuned to show. */
@media (min-width: 1025px) {
  .review-card {
    width: clamp(390px, 38vw, 540px);
  }
}

@media (max-width: 680px) {
  .review-card {
    width: min(80vw, 340px);
  }
}

/* On a narrow phone the card is only as wide as the screen allows, and three lines of
   preview plus a wrapped row of tags made it taller than it is wide — a banner that had
   turned back into a portrait card. Two lines here: the shape is what the client asked
   for, and the whole review is one tap away regardless. */
@media (max-width: 480px) {
  .review-card__body {
    max-height: 2lh;
  }

  .review-card__tags li {
    font-size: clamp(8.5px, 0.506vw, 15.3px);
    letter-spacing: 0.08em;
  }
}

/* PHONE TYPE SCALE — 2026-08-10.
 *
 * Measured at 390px, this route did not have a type scale: visible content ran
 * from 8px labels straight to a 93.6px h1. The office values sat at 22.62px
 * beside 8px labels, while section headings were 30px and the footer banner
 * jumped to 40px. That is why the page felt simultaneously too large and too
 * small. These six values are the phone experiment the client requested. They
 * are shared from style.css, mapped to Contact's existing local names here,
 * and leave tablet/desktop untouched. */
@media (max-width: 680px) {
  .contact-page {
    --contact-phone-label: var(--phone-type-label);
    --contact-phone-body: var(--phone-type-body);
    --contact-phone-control: var(--phone-type-control);
    --contact-phone-subheading: var(--phone-type-subheading);
    --contact-phone-heading: var(--phone-type-heading);
    --contact-phone-display: var(--phone-type-display);
  }

  .contact-page .contact-kicker,
  .contact-page .contact-primary-action,
  .contact-page .contact-section-link,
  .contact-page .office-detail dt,
  .contact-page .office-map > a,
  .contact-page .enquiry-form__heading > p,
  .contact-page .enquiry-field label,
  .contact-page .enquiry-error,
  .contact-page .review-card__tags li,
  .contact-page .review-card__name,
  .contact-page .review-card__detail,
  .contact-page .footer-bottom {
    font-size: var(--contact-phone-label);
  }

  .contact-page .contact-kicker {
    margin-bottom: 18px;
    letter-spacing: 0.14em;
  }

  .contact-page .contact-hero h1 {
    font-size: var(--contact-phone-display);
    letter-spacing: -0.055em;
    line-height: 0.92;
  }

  .contact-page .contact-hero__statement {
    margin-top: 18px;
    font-size: 17px;
    line-height: 1.3;
  }

  .contact-page .contact-hero__actions {
    margin-top: 24px;
  }

  .contact-page .contact-primary-action,
  .contact-page .contact-section-link,
  .contact-page .office-detail dt,
  .contact-page .office-map > a,
  .contact-page .enquiry-form__heading > p,
  .contact-page .enquiry-field label {
    letter-spacing: 0.12em;
  }

  .contact-page .contact-section-heading h2,
  .contact-page .puzzle h2 {
    font-size: var(--contact-phone-heading);
    letter-spacing: -0.035em;
    line-height: 1.08;
  }

  .contact-page .office-detail dd {
    font-size: 18px;
    line-height: 1.3;
  }

  .contact-page .enquiry-form__heading h3 {
    font-size: var(--contact-phone-subheading);
    line-height: 1.15;
  }

  .contact-page .enquiry-form__heading > span {
    font-size: 14px;
    line-height: 1.4;
  }

  .contact-page .enquiry-field input,
  .contact-page .enquiry-field select,
  .contact-page .enquiry-field textarea {
    font-size: var(--contact-phone-control);
    line-height: 1.4;
  }

  .contact-page .review-card__body,
  .contact-page .puzzle__lede {
    font-size: var(--contact-phone-body);
    line-height: 1.35;
  }

  .contact-page .review-card__zh {
    font-size: 14px;
    line-height: 1.35;
  }

  .contact-page .review-card__tags li {
    padding: 2px 5px;
    letter-spacing: 0.06em;
  }

  .contact-page .review-card__name,
  .contact-page .review-card__detail,
  .contact-page .review-card__cue::after {
    font-size: var(--contact-phone-label);
    letter-spacing: 0.08em;
  }

  .contact-page .puzzle__time {
    font-size: 22px;
  }

  /* The footer's phone sizes used to be restated here — 20px for the
     acknowledgement and 30px for the band's heading. They were written when the
     shared footer was still large, and they now only make this one page's
     footer bigger than every other page's. style.css sets 16px for both, on
     every page, and this page follows it. Nothing about the footer belongs in a
     per-page sheet: see the note beside `.site-footer` in style.css. */
}
