/* ============================================================
   Ink & Intuition — Closing CTA (#book)
   The one centred moment on the page: a held breath before the
   footer. No card, no glass panel, no form — just the copy and
   two slow, heavily blurred aura blobs breathing behind it.

   AMBIENT BEHAVIOUR: CONVERGENCE, BREATH AND REST. Last chapter
   of the page's narrative (diffusion -> gathering -> surface
   tension -> buoyancy -> refracted light -> AFTERGLOW), and the
   quietest: roughly a third of the hero's intensity, less than
   anything above it. Violet arrives gently from the left, amber
   with a restrained coral from the right, and both lean slowly
   toward the centred call to action and settle back. Large areas
   of clean cream in between are the point; the two fields must
   never meet in the middle as a bright gradient.
   ============================================================ */

.cta {
  text-align: center;
  overflow: clip;

  /* ---------- Tonal ramp: the deep end ----------
     Last of the three bottom chapters. Who runs cool, About runs a warm
     cream-rose, and this one has the job the other two do not: the page went
     from bright cream straight into a near-black footer (--ink-900) in one row,
     which is the abrupt hand-off Daniel reported. The full reasoning behind the
     ramp, the luminance argument and the seam-lip rule are written out once over
     .who in who.css.

     Two layers, doing two different jobs.

     --cta-wash-warm is the chapter's own light, an amber continuation of About's
     rose. It starts at zero at the section's top edge, because that edge is the
     amber seam and its lip is bare paper; by the time the wash is at strength
     the join is 200px behind. It does NOT fade out at the bottom: there is no
     seam between this chapter and the footer, so the bottom edge is the one
     place on the page where a wash is allowed to run right off the sheet.

     --cta-foot-* is the hand-off itself. Read it as the footer's ink slab lying
     on the paper and shading it, which is the same story the seam lips already
     tell one join up, only at the scale of a whole block instead of a lifted
     edge. It runs over --cta-footfall from nothing to a warm mauve the sheet
     genuinely could be at, in a hue that sits between the amber above it and the
     violet-black below it rather than turning to mud. Measured on the rendered
     page with tools/ramp-joins.mjs, it takes the step at the footer's top edge
     from 75.2 / 74.4 / 73.9 CIE L* units (1440 / 2552 / 390) down to
     39.5 / 37.7 / 40.2, so the join reads as the light running out rather than
     as a cut. Re-run that probe after any change here; BEFORE=1 reproduces the
     pre-ramp page.

     Gradients, not a blend mode, and this is the element's own background, below
     .cta-blobs and everything else. Nothing here blends against a backdrop, so
     the transparent-backdrop trap cannot apply.

     --cta-footfall is kept off the copy on purpose. The section carries extra
     bottom padding below (a held breath before the footer was always the brief),
     which keeps the last line of copy out of the ramp's working range. That
     relationship survived the 2026-07-26 spacing trim -- see --cta-pad-* -- but
     it is the reason the bottom padding is larger than the top and it must be
     re-measured with tools/ramp-contrast.mjs at 1440, 2552 AND 390 if either
     number moves again. */
  --cta-footfall: clamp(150px, 17vh, 250px);

  --cta-wash-warm: rgba(251, 214, 176, 0.30);
  --cta-wash-warm-0: rgba(251, 214, 176, 0);

  /* Zero-alpha end stop spelled out, as with --sheet-contact-0 in base.css. */
  --cta-foot-0: rgba(122, 74, 44, 0);
  --cta-foot-1: rgba(122, 74, 44, 0.13);
  --cta-foot-2: rgba(94, 58, 46, 0.36);
  --cta-foot-3: rgba(62, 44, 52, 0.58);

  /* ---------- Spacing ----------
     This used to run the full --section-pad top (168px at 1440 and up) plus an
     extra bottom (216.96px measured), which made the closing chapter the
     largest block on the page and read as an empty gap rather than as a held
     breath. 128 / 136 at desktop is the target; the bottom stays the larger of
     the two because it is still doing the --cta-footfall job described above.

     Both clamps bottom out at the values 390 already had (96 / 120), so the
     phone layout is byte-identical to before the trim -- deliberate, because at
     390 the pill already sits inside the ramp's first fifth and there is no
     headroom there to spend. */
  --cta-pad-top: clamp(96px, 9vw, 128px);
  --cta-pad-bottom: clamp(120px, 9.5vw, 136px);
  padding-block: var(--cta-pad-top) var(--cta-pad-bottom);

  background-image:
    linear-gradient(180deg,
      var(--cta-foot-0) 0%,
      var(--cta-foot-1) 46%,
      var(--cta-foot-2) 78%,
      var(--cta-foot-3) 100%),
    linear-gradient(180deg,
      var(--cta-wash-warm-0) 0%,
      var(--cta-wash-warm) 32%,
      var(--cta-wash-warm) 100%);
  background-position: bottom center, top center;
  background-size: 100% var(--cta-footfall), 100% 100%;
  background-repeat: no-repeat, no-repeat;
}

/* ---------- Aura blobs: the convergence ----------

   The section's one ambient behaviour. Each field leans a little way toward the
   centred call to action, over-relaxes slightly on the way back, and rests. It
   is authored in CSS keyframes rather than in js/sections/cta.js (which used to
   drive it with GSAP) for three reasons: transform and opacity are the only two
   properties involved so it is compositor-only either way; the shared ambient
   ceilings in tokens.css are CSS custom properties, and reading them back into
   JS to obey them is pure indirection; and base.css's reduced-motion rule
   already collapses a CSS animation correctly, which a GSAP tween has to be
   told about separately.

   Amplitudes are the published ceilings, not new numbers: travel <=
   --atmos-drift (18px), scale <= --atmos-scale (1.045), opacity swing well
   under --atmos-fade (0.12). The GSAP version ran 34px / 1.06 over 15s, i.e.
   past all three of them.

   The two blobs run on DIFFERENT primes (29s and 23s) so their leans never
   coincide. That is what keeps rule 4 of the brief: two pools of light quietly
   moving toward a conversation, never both at full lean at once, so they cannot
   collide into a bright gradient in the middle. */

.cta-blobs {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
}

.cta-blob {
  position: absolute;
  border-radius: 50%;
  /* Both per-blob and both pointing INWARD, toward the centred CTA. Defaults
     here so an unmodified .cta-blob is still legible: an unresolvable var() in
     `opacity` computes to 1, which would be a very loud accident. */
  --cta-lean-x: 0px;
  --cta-lean-y: 0px;
  --cta-blob-rest: 0.3;
  --cta-blob-peak: 0.35;
  opacity: var(--cta-blob-rest);
  will-change: transform, opacity;
  animation: cta-lean var(--dur-ambient-slow) var(--ease-ambient) infinite;
}

/* 0% and 100% are the RESTING state, not an endpoint of the travel: the blob
   sits exactly where its own left/top/width/height put it. That matters because
   base.css collapses animation-duration to 0.01ms and iteration-count to 1
   under prefers-reduced-motion, so the last frame a reduced-motion visitor sees
   is this one. Held still, fully visible, never parked mid-drift and never
   faded out. */
@keyframes cta-lean {
  0%, 100% {
    transform: translate3d(0, 0, 0) scale(1);
    opacity: var(--cta-blob-rest);
  }
  34% {
    transform: translate3d(var(--cta-lean-x), var(--cta-lean-y), 0) scale(var(--atmos-scale));
    opacity: var(--cta-blob-peak);
  }
  68% {
    transform: translate3d(calc(var(--cta-lean-x) * -0.4), calc(var(--cta-lean-y) * -0.4), 0) scale(0.986);
    opacity: var(--cta-blob-rest);
  }
}

.cta-blob--violet {
  left: 4%;
  top: -8%;
  width: min(620px, 60vw);
  height: min(620px, 60vw);
  /* Was .4, which made the top-left corner the loudest thing in the chapter and
     read as a grey-mauve haze rather than as violet light -- violet over cream
     subtracts yellow, so the more of it there is the less warm the corner gets.
     "Gently from the left" is the brief. */
  --cta-blob-rest: 0.3;
  --cta-blob-peak: 0.35;
  /* Down and to the right: this blob starts above the section's top-left. */
  --cta-lean-x: calc(var(--atmos-drift) * 0.9);
  --cta-lean-y: calc(var(--atmos-drift) * 0.5);
  filter: blur(clamp(64px, 8vw, 110px));
  /* Plain violet fallback first; color-mix layer quietly pulls the blob
     toward --mood-accent when a visitor has picked a mood in the hero.
     Browsers without color-mix() simply keep the plain-violet rule above.
     Zero stops are spelled rgba(r,g,b,0) and never `transparent`, which is
     rgba(0,0,0,0): see the --atmos-* block in tokens.css. */
  background: radial-gradient(circle at 42% 38%, var(--violet) 0%, rgba(143, 131, 224, 0) 72%);
  background: radial-gradient(circle at 42% 38%, color-mix(in srgb, var(--violet) 64%, var(--mood-accent) 36%) 0%, rgba(143, 131, 224, 0) 72%);
}

.cta-blob--amber {
  right: 2%;
  bottom: -12%;
  width: min(560px, 54vw);
  height: min(560px, 54vw);
  --cta-blob-rest: 0.3;
  --cta-blob-peak: 0.35;
  /* Up and to the left: this one starts below the section's bottom-right. */
  --cta-lean-x: calc(var(--atmos-drift) * -0.85);
  --cta-lean-y: calc(var(--atmos-drift) * -0.55);
  /* The shorter prime, so the two leans drift permanently out of phase. */
  animation-duration: var(--dur-ambient);
  filter: blur(clamp(60px, 7vw, 100px));
  /* Amber core with a restrained coral shoulder, which is the same arc the
     primary button's --grad-cta runs, only a whisper of it. */
  background: radial-gradient(circle at 58% 60%,
    var(--amber) 0%,
    var(--coral-soft) 46%,
    rgba(248, 129, 93, 0) 76%);
}

/* ---------- Metaballs: the rising gooey field (2026-07-27) ----------
   A third, much finer-grained ambient behaviour layered inside .cta-blobs,
   after the two aura blobs so it paints on top of their broad wash. Mounted
   by js/sections/cta.js from the SHARED js/ambient/metaballs.js module (its
   own header has the technique and the reusable API); css/ambient/
   metaballs.css owns the structural rules for .ambient-metaballs and its
   children. Nothing CTA-specific needs to live here: .cta-metaballs inherits
   position:absolute + inset:0 from the shared stylesheet, and inside
   .cta-blobs (itself absolute + inset:0) that already fills the whole
   section at any width, including 2552, so it needs no width cap to "span
   the whole section" -- it structurally cannot bunch to the centre.

   Palette, opacity, particle cap and timing are CTA's own tuning and live in
   js/sections/cta.js next to the numbers they were measured against, not
   here, so the reasoning for "why 0.16 opacity" stays beside the option that
   sets it rather than split across two files.

   z-index: none needed. .cta-metaballs is the last child of .cta-blobs
   (z-index 0), so it paints above the two blobs and still below
   .cta-container (1) and the grain overlay (2), same stacking rule as
   everything else in this file. */

/* ---------- Pill variant glow (2026-08-20, PILL-VARIANTS, ?pill=a) ----------
   Daniel: "bottom pill isn't updated at all" - PILL had already copied the
   hero pill's glass recipe onto .cta-locations verbatim and measured that
   the computed values matched at every width (see PILL's own comment on
   .cta-locations below). The recipe was never the problem. The hero pill
   reads as glass because it floats over Jess's photograph; blurring quiet
   cream at 16px instead of 6px is still blurring quiet cream, and this
   section's own two aura blobs sit at the CORNERS (violet top-left, amber
   bottom-right), not under the centred pill. This element is the fix for
   THAT, not another pass at the material: a contained pool of real colour,
   sitting directly under where the pill lands, so the frosted glass
   finally has something to refract - exactly the BRAND-KIT rule this
   section's own aura blobs and the enquiry modal's aura already follow
   ("glass only reads as glass when something colourful sits behind it").

   MUST be a sibling of .cta-locations, sitting in .cta-blobs' z-index:0
   layer - one level BELOW .cta-container (z-index 1, which holds
   .cta-locations) - and not a pseudo-element hung off .cta-locations
   itself. backdrop-filter samples what is visually BEHIND an element in
   paint order; a child or ::before/::after of .cta-locations paints INSIDE
   its own box, on top of the backdrop that same element's backdrop-filter
   already resolved, not part of it. Gotcha 7 territory: this element needs
   real content of ITS OWN to be visible through (its background), but nothing
   here uses backdrop-filter or mix-blend-mode, so the "unpainted corner"
   half of that gotcha does not arise - this is a plain filter:blur() on a
   fully opaque-centred radial gradient, closer kin to .cta-blob than to
   anything glass.

   POSITIONED OFF --cta-pad-bottom, NOT A MAGIC NUMBER. .cta-locations is
   the last child of .cta-container, a shrink-to-fit flex column, so its own
   bottom edge sits almost exactly --cta-pad-bottom above .cta's bottom
   edge at every width - the same custom property .cta itself uses for its
   padding, inherited straight down since custom properties cascade. Anchor
   the glow's bottom edge there with `bottom: var(--cta-pad-bottom)`, then
   translateY(50%) - not left where set - to slide the box down by half its
   OWN height, which lands its centre (not its edge) on that line. The glow
   is sized much taller than the pill itself, so the small remaining gap
   between "the pill's bottom edge" and "the pill's vertical centre" (its
   own half-height, a few tens of px) is well inside the glow's own soft
   falloff at every one of 2552 / 1440 / 390 - verified visually, not just
   arithmetically, in tools/pill-variants-2026-08-20.mjs. Horizontal centring
   needs no such reasoning: .cta-container is centred in .cta (text-align:
   center + margin-inline:auto), so left:50% on this element (also
   inset:0 inside .cta-blobs, i.e. sized off .cta's own box) lines up with
   the pill's own horizontal centre at every width for free.

   Colour reasoning (what it is now and why an earlier draft changed) is in
   the comment directly below this rule, next to the rule it explains.

   Geometry only in this rule. It shipped flag-gated and invisible by
   default; Daniel chose it on 2026-08-20, so the paint now lives in the
   rule below and the OFF states are the exception rather than the rule. */
.cta-pill-glow {
  position: absolute;
  left: 50%;
  bottom: var(--cta-pad-bottom);
  transform: translate(-50%, 50%);
  width: min(560px, 78vw);
  height: clamp(180px, 22vw, 240px);
  border-radius: 50%;
  pointer-events: none;
}

/* First pass here was a 55/45 violet/amber mix (rgb(190,152,166)) at .85
   opacity: correctly live (confirmed by getComputedStyle - see the probe),
   but a real headed-Chrome screenshot at 390 and 1440 showed it read as
   almost nothing next to the control, failing this brief's own acceptance
   bar ("visibly different at a glance"). The reason is arithmetic, not a
   rendering bug: violet and amber sit on roughly opposite sides of the
   brand's hue arc, so mixing them desaturates rather than combines, and
   whatever survives that is then diluted again by the pill's own fixed 62%
   white fill (untouchable - "keep the hero recipe EXACTLY as-is" is this
   variant's own premise). --pink-deep is the fix: still the CTA's own arc
   (violet -> pink -> coral -> amber, the same order the enquiry beam
   rotates, section header comment above), but a single saturated hue
   survives two rounds of dilution far better than a pre-muddied blend
   does. A flat plateau (0% and 34% both full colour) before the fade
   starts, rather than fading from the very centre, is what stops the
   result from reading as a soft haze with no body: verified in the same
   real-Chrome screenshots this replaced. Zero-stop is --pink-deep's own
   rgb (217, 90, 143) at alpha 0, not a different hue fading to black -
   same convention as the two .cta-blob rules above it. */
/* CHOSEN BY DANIEL, 2026-08-20, at half the intensity it was pitched at:
   "reduce colour pool intensity by about 50% and the A".

   So this is no longer flag-gated - it is what the section ships - and the
   opacity is 0.46 rather than the 0.92 the variant was built at. 0.92 was
   deliberately pitched loud so the A/B/C comparison could not be missed in a
   screenshot, and the agent that built it said so at the time; it was always
   louder than this section's own header comment allows for ("roughly a third
   of the hero's intensity ... the quietest chapter"). Halving the opacity is
   the honest reading of "50%": it halves the alpha the pool composites at,
   and leaves the hue, the plateau shape and the blur exactly as tuned, so
   nothing about the recipe's character changes, only its strength.

   The pill's own material is untouched by all of this. That is the entire
   point of variant A and it is worth restating here, because it is the fix
   for the thing that went wrong first: the hero-pill glass recipe was
   already correct on this element and still read as nothing, because over
   quiet cream a backdrop-filter has nothing to bend. Giving the glass
   something behind it to refract is what made it read as glass. Do not
   "simplify" this by pushing the pill's own background more opaque - that
   trades the glass back for a sticker, which is the 2026-07-26 objection
   quoted in .cta-locations above. */
.cta-pill-glow {
  opacity: 0.46;
  background: radial-gradient(ellipse at center,
    var(--pink-deep) 0%,
    var(--pink-deep) 34%,
    rgba(217, 90, 143, 0) 74%);
  filter: blur(34px);
}

/* The comparison stays reachable so this is one URL to re-judge rather than
   a rebuild: ?pill=none is the old flat-cream control, and b / c are the two
   treatments that were not chosen (both are pill-material changes, so
   neither wants A's pool behind it). */
html[data-pill-variant='none'] .cta-pill-glow,
html[data-pill-variant='b'] .cta-pill-glow,
html[data-pill-variant='c'] .cta-pill-glow {
  opacity: 0;
  background: none;
  filter: none;
}

/* ---------- Fine grain ----------
   The unifying finish. Every other light section paints this tile at
   --grain-size / --grain-opacity / soft-light and this chapter had no grain
   layer at all, which was visible as a change of surface at the amber seam.

   Blend safety: .cta sets `position: relative` (from .section) and `overflow:
   clip`, and NEITHER creates a stacking context, so this overlay blends against
   what .content-sheet has painted -- the veiled paper, plus this section's own
   two background gradients. There is no unpainted region inside .cta to fall
   through on: the sheet's border-radius is top-only (`--sheet-radius
   --sheet-radius 0 0`), which is why the guard base.css puts on
   .approach::after is not needed here. Do NOT add `isolation: isolate` or a
   background to .cta to "tidy this up"; that is the documented way to break it.

   The URL is spelled out rather than tokenised because a relative url() inside
   a custom property resolves against the stylesheet that USES it. Trap 2 in
   docs/ENGINEERING-NOTES.md.

   z-index 2 puts it above .cta-container (1) and the blobs (0), matching every
   other section, and below the amber seam lip (6) so the join stays bare paper.
   Measured over the footer hand-off: a soft-light mid-grey tile leaves an
   already-dark backdrop alone, so the ramp and the button do not go grey. */
.cta::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  background: url('../../assets/img/grain.png') center / var(--grain-size) repeat;
  mix-blend-mode: var(--grain-blend);
  opacity: var(--grain-opacity);
}

/* ---------- Content ---------- */

.cta-container {
  position: relative;
  z-index: 1;
  max-width: 680px;
  margin-inline: auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(26px, 3.6vw, 38px);
}

.cta-body {
  max-width: 48ch;
  color: var(--ink-600);
}

.cta-actions {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 18px;
}

/* ---------- Blush halo ----------
   A very faint warmth sitting under the primary button, so the button reads as
   the brightest point of the afterglow rather than as a control dropped onto a
   flat field. --atmos-pink-2 (.07) is the whole of it, falling to zero well
   inside the .cta-alt line below.

   z-index -1 is safe here specifically because .cta-container is a stacking
   context (position: relative + z-index: 1), so the halo cannot escape behind
   the section's own wash; it paints above the container's background and below
   the button and the alt line. .cta-actions itself stays z-index: auto so it
   does NOT trap it one level too high.

   Anchored to the button's centre line rather than to .cta-actions' box, which
   also contains the alt paragraph: the button is 16px padding + 15px/1 type =
   47px tall, so its centre is 23px down. Being a few pixels out costs nothing
   on a soft ellipse.

   Wide and short, and the falloff is deliberately steeper than it looks like it
   needs to be. .cta-alt is the tightest contrast target in this chapter and a
   pink tint costs it fast (relative luminance is 71.5% green). Measured with
   tools/ramp-contrast.mjs at 1440: a 210px-tall halo reaching zero at the box
   edge took .cta-alt's p5 ratio from 5.12 to 4.89, which is closer to the 4.5
   floor than anything on this page should sit. At 160px reaching zero at 92%
   the alt line sees roughly a third of that. Re-run that probe if either number
   moves. */
.cta-actions::before {
  content: '';
  position: absolute;
  z-index: -1;
  left: 50%;
  top: 0;
  width: min(460px, 84vw);
  height: 160px;
  transform: translate(-50%, calc(-50% + 23px));
  border-radius: 50%;
  pointer-events: none;
  background: radial-gradient(closest-side,
    var(--atmos-pink-2) 0%,
    var(--atmos-pink-1) 40%,
    var(--atmos-pink-0) 92%);
}

.cta-alt {
  font-size: var(--text-small);
  color: var(--ink-600);
}

.cta-email {
  display: inline-block;
  overflow-wrap: anywhere;   /* the long address can wrap on narrow phones instead of clipping */
  color: var(--violet-ink);
  font-weight: 600;
  text-decoration: underline;
  text-decoration-color: rgba(75, 63, 143, 0.35);
  text-underline-offset: 3px;
  transition: color var(--dur-quick) var(--ease-brand), text-decoration-color var(--dur-quick) var(--ease-brand), transform var(--dur-quick) var(--ease-brand);
}
.cta-email:hover {
  color: var(--pink-link);
  text-decoration-color: currentColor;
}
.cta-email:active {
  transform: scale(0.98);
}

.cta-locations {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ink-700);
  width: max-content;
  padding: 13px 24px;
  /* HERO-PILL GLASS, reproduced verbatim per Daniel 2026-08-20 ("same look as
     the hero title ... same bubble style as that one"). This REPLACES the
     2026-07-26 "flat-surface chip" decision below (kept as a comment, not
     deleted, because the reasoning is still real and the next agent who
     wants to change this pill needs to know it exists):

       "FLAT-SURFACE CHIP, not floating glass. This carried the glass recipe
       until 2026-07-26: a 1px rgba(255,255,255,.68) border, a white-to-white
       gradient over a tri-colour tint, blur(18px) saturate(1.32) and an
       inset white highlight. That recipe is for a panel over colourful
       imagery. Over quiet cream there is nothing for it to refract, and a
       glowing white border on a cream field reads as a sticker rather than
       as a surface."

     That objection was about a DIFFERENT, heavier recipe (blur 18 sat 1.32,
     tri-colour tint) sitting over quiet cream paper. The brief this time is
     an explicit, named instruction to copy hero.css:511's recipe raw-value
     for raw-value, and what actually paints behind THIS pill is not bare
     cream: .cta-blobs (violet top-left, amber bottom-right, both heavily
     blurred) is position:absolute + inset:0 on the whole section, so it
     extends behind the container's last child same as its first. See the
     PILL findings for the measured verdict on whether that is enough to
     refract - reverting this block to the flat-chip recipe above is the
     correct fix if it is not.

     Raw values, not tokens, for the same reason hero.css:506-510 gives: this
     is a specific approved recipe being reproduced, not a new material.
     -webkit line MUST stay first - see ways.css's own load-bearing-order
     comment; the plain `backdrop-filter` after it is WebKit's intentional
     fallback to plain blur, not a bug. */
  background: rgba(255, 255, 255, 0.62);
  -webkit-backdrop-filter: blur(16px) saturate(1.3);
  backdrop-filter: blur(16px) saturate(1.3);
  border: 1px solid rgba(255, 255, 255, 0.92);
  border-radius: var(--radius-pill);
  box-shadow: 0 20px 40px -16px rgba(42, 34, 48, 0.35), inset 0 0 0 1px rgba(255, 255, 255, 0.4), inset 0 1px 1px rgba(255, 255, 255, 0.85);
}

/* ---------- ?pill=none: pre-PILL-VARIANTS flat-chip control ----------
   Keep this as an explicit override, rather than merely hiding A's colour
   pool: it is the exact computed material and geometry before the current
   hero-pill treatment replaced it. The avatar is decorative C-only markup;
   hiding it here keeps the comparison to the original plain location string
   honest as well as visually equivalent. */
html[data-pill-variant='none'] .cta-locations {
  width: auto;
  padding: 10px 17px;
  border: 1px solid var(--ink-border);
  border-radius: var(--radius-pill);
  background: rgba(255, 255, 255, 0.55);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  box-shadow: none;
}

html[data-pill-variant='none'] .cta-locations-avatar {
  display: none;
}

/* Decorative children PILL-VARIANTS' ?pill=c anatomy needs (see the markup
   comment in 07-cta.html). Hidden/inline by default so no-flag / ?pill=a /
   ?pill=b render byte-identical to the plain string this replaced: an
   unstyled <span> is display:inline, so .cta-locations-line1, its sep and
   .cta-locations-line2 simply flow together as one run of text under
   .cta-locations' own font-family/size/letter-spacing/text-transform,
   exactly as the flat string did. Only .cta-locations-avatar needs an
   explicit off-state, since a bare span with real text content ("J") would
   otherwise sit inline in front of the rest of the line. */
.cta-locations-avatar { display: none; }

/* ---------- ?pill=b: a defined bubble, not a backdrop ----------
   Same material (background/backdrop-filter/border-radius all untouched,
   inherited from the block above), but this variant stops asking cream
   paper for something it does not have. A crisp two-tone edge - the old
   flat-chip recipe's dark hairline (--ink-border was rgba(42,34,48,.16) on
   bare cream) thickened and darkened well past that starting point - plus
   the hero recipe's own inset white highlight, kept - and a NEAR shadow
   added alongside the existing far one, so the pill reads as an object
   sitting a little above the page rather than a tint of it. box-shadow is
   a full replacement, not additive, so every layer the base rule had is
   repeated here alongside the new ones.

   .3 alpha / 1.5px was the first pass here, correctly live
   (getComputedStyle confirmed it) but, same finding as ?pill=a, a real
   screenshot at 390 and 1440 showed it reading as near-nothing next to the
   control - a dark hairline that faint gets lost against a border that was
   already there (rgba(255,255,255,.92), just a light one), and the shadow
   swap alone was not carrying the difference on its own. .42 alpha / 2px,
   plus a tight, comparatively opaque contact shadow close under the pill
   (the second box-shadow layer, unlike the base recipe's one soft/far
   shadow) is what actually reads as "an edge" and "resting on the page"
   at a glance rather than under measurement - verified in the same
   real-Chrome screenshots this replaced. */
html[data-pill-variant='b'] .cta-locations {
  padding: 18px 34px;
  border: 2.5px solid rgba(42, 34, 48, 0.48);
  box-shadow:
    0 34px 58px -16px rgba(42, 34, 48, 0.52),
    0 8px 20px -4px rgba(42, 34, 48, 0.4),
    inset 0 0 0 1px rgba(255, 255, 255, 0.6),
    inset 0 1px 1px rgba(255, 255, 255, 0.95);
}

/* ---------- ?pill=c: the hero pill's actual anatomy ----------
   Daniel's most literal reading ("same bubble style as that one"): not just
   the material, the OBJECT - monogram avatar + two stacked lines, copied
   from .hero-pill / .hero-pill-avatar / .hero-pill-text / .hero-pill-years
   / .hero-pill-cred (hero.css:511-548). Gap (12px) and both type sizes
   (13px / 11px) are that recipe's numbers verbatim; only the avatar comes
   down a touch, 36px against hero's 40px, because this pill is read on
   quiet paper at reading distance, not afloat over a portrait as a corner
   accent competing with a whole face for attention.

   text-align: left is the one override earned here rather than inherited:
   .cta sets text-align:center for the section's single-column copy stack,
   which the flat one-line pill never had to fight (a width:max-content
   block has nothing left to centre against), but two ragged-length lines
   stacked next to a fixed avatar column read as a mistake if centred - see
   .hero-pill-text, which is left-aligned for the same reason and inherits
   it for free because nothing in the hero stage forces centre. */
html[data-pill-variant='c'] .cta-locations {
  display: flex;
  align-items: center;
  gap: 12px;
  text-align: left;
  padding: 11px 22px 11px 11px;
}

html[data-pill-variant='c'] .cta-locations-avatar {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 36px;
  height: 36px;
  border-radius: 50%;
  background: var(--grad-monogram);
  font-family: var(--font-serif);
  font-style: italic;
  font-size: 18px;
  color: #fff;
}

html[data-pill-variant='c'] .cta-locations-text {
  display: flex;
  flex-direction: column;
  line-height: 1.25;
}

/* The flat string's middle separator becomes the line break; the first one
   (inside "Blue Mountains &middot; Sydney") stays, it is now INSIDE line 1. */
html[data-pill-variant='c'] .cta-locations-sep { display: none; }

html[data-pill-variant='c'] .cta-locations-line1 {
  font-family: var(--font-sans);
  font-weight: 600;
  font-size: 13px;
  letter-spacing: normal;
  text-transform: none;
  color: var(--ink-900);
}

html[data-pill-variant='c'] .cta-locations-line2 {
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.05em;
  color: var(--ink-700);
}

/* ---------- Mobile collapse (<768px) ---------- */

@media (max-width: 767px) {
  .cta-container {
    gap: 22px;
  }

  .cta-locations {
    max-width: 92vw;
    font-size: 0.6875rem;
    line-height: 1.6;
    letter-spacing: 0.12em;
  }

  .cta-blob--violet {
    width: 80vw;
    height: 80vw;
    left: -12%;
    top: -10%;
    filter: blur(56px);
  }

  .cta-blob--amber {
    width: 72vw;
    height: 72vw;
    right: -16%;
    bottom: -12%;
    filter: blur(52px);
  }

  /* ?pill=b at 390: desktop's 16px/30px would eat further into the 92vw
     ceiling above than a hairline-and-shadow variant needs to; a smaller
     bump over control's 13px/24px keeps the "defined object" read without
     forcing an earlier wrap than the other variants get. */
  html[data-pill-variant='b'] .cta-locations {
    padding: 15px 26px;
  }

  /* ?pill=c at 390: hero's OWN mobile numbers for this exact anatomy
     (hero.css:690-700, the "mobile mockup" breakpoint), reused rather than
     re-derived - the brief's bar is "same bubble style as that one" and
     that includes how it compresses. */
  html[data-pill-variant='c'] .cta-locations {
    gap: 9px;
    padding: 9px 16px 9px 9px;
  }
  html[data-pill-variant='c'] .cta-locations-avatar {
    width: 30px;
    height: 30px;
    font-size: 16px;
  }
  html[data-pill-variant='c'] .cta-locations-line1 { font-size: 11.5px; }
  html[data-pill-variant='c'] .cta-locations-line2 { font-size: 9.5px; }
}

@media (prefers-reduced-transparency: reduce) {
  .cta-locations {
    background: rgba(255, 250, 246, 0.96);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
}

/* Decoration only, and the forced palette is the point of this mode. The footer
   hand-off is not needed there either: forced colours give the footer and the
   sheet their own guaranteed contrast. The grain goes with them: a soft-light
   noise tile over a forced background is the one place it can only do harm. */
@media (forced-colors: active) {
  .cta { background-image: none; }
  .cta::after { display: none; }
}

/* ============================================================
   THE ENQUIRY MODAL'S BORDER BEAM (2026-07-27)
   ============================================================
   Daniel: "can you make a border beam that animates on a rotation with our
   brand gradient similar to this look on the enquiry overlay that pops up",
   with Aceternity's BackgroundGradient attached as the reference for THE LOOK.

   WHY THIS BLOCK IS IN cta.css AND NOT NEXT TO THE REST OF THE MODAL.
   Everything else about .enquiry lives in css/base.css. base.css is a shared
   file this pass does not own, and cta.css is loaded after it, so the beam is
   authored here as a pure addition: not one existing .enquiry rule is
   overridden, and the only thing added to base.css's world is two new spans
   inside .enquiry-card (sections/07-cta.html). If the modal's styles are ever
   given a stylesheet of their own, move this block there whole.

   ---- WHAT THE REFERENCE DOES, AND WHERE THIS DEPARTS FROM IT ----

   The reference is React + framer-motion + Tailwind; this is static CSS. Its
   construction is two stacked absolutely-positioned layers carrying the same
   four-corner `radial-gradient(circle farthest-side at <corner>)` stack, the
   back one heavily blurred at opacity .6 and the front one crisp, both sized
   `background-size: 400% 400%` and animated by moving `background-position`
   from `0 50%` to `100% 50%` and back over 5s. That travelling background is
   what reads as rotation.

   TWO DELIBERATE DEPARTURES.

   1. IT ROTATES FOR REAL, ON A TRANSFORM, RATHER THAN TRAVELLING ON
      background-position. Animating `background-position` on a 400%-sized
      four-radial stack repaints the whole layer every frame on the paint
      thread; a `transform: rotate()` on an oversized child inside an
      `overflow: hidden` ring is painted once and then moved by the
      compositor. Same look, and it is the honest reading of "animates on a
      rotation" besides. The one per-frame cost left is the glow's blur (see
      the note on --glow below), which is a GPU pass, not a repaint.
   2. OUR PALETTE, AT OUR STRENGTH. The reference is teal / violet / amber /
      blue on near-black at full saturation. This is a warm booking form on a
      psychotherapy site, so the four pools are the site's own arc --
      violet-deep -> pink -> coral -> amber, the same order --grad-cta and the
      CTA metaballs already run -- laid out in rotational order so the sweep
      reads as that arc travelling rather than as four unrelated lights. It has
      to read as a soft lit edge, not as an RGB halo.

   ---- THE TRAP THIS BLOCK WAS WRITTEN AROUND ----

   THERE IS NO `will-change` ANYWHERE IN HERE AND THERE MUST NOT BE. The
   reference puts `will-change: transform` on both animated layers. The
   enquiry card is real glass: .enquiry-card__rim and .enquiry-card__surface
   both carry a working `backdrop-filter`. `will-change` makes an element a
   backdrop root in Chrome and silently disables every backdrop-filter inside
   it, and the card still looks almost right when it happens, which is how it
   has already cost this project a round (docs/ENGINEERING-NOTES.md, "Two ways
   backdrop-filter lies"). Chrome promotes a compositor-driven transform
   animation on its own; the hint buys nothing here and costs the material.

   The same rule is why both beam layers are SIBLINGS of the glass and never
   ancestors of it. --glow carries a `filter` and --edge carries a `mask`, and
   either one on .enquiry-card or on .enquiry would kill the frost. As
   siblings they are simply painted into the backdrop the glass samples, which
   is why the rim visibly refracts the glow rather than sitting in front of it.

   Verified, not asserted: tools/enquiry-beam.mjs sweeps the face's own blur
   from 0px to 96px and reports transmitted stripe contrast at both ends. If
   the two numbers are the same the filter is not running, whatever
   getComputedStyle says. See that file's header for the before/after figures.

   ---- NO VISIBILITY GATE, ON PURPOSE ----

   A <dialog> without `open` is `display: none` in the UA stylesheet, so while
   the modal is closed these elements have no layout box and the animation
   cannot run. There is nothing to pause and nothing to start; adding an
   is-open gate would be a rule that never changes anything. */

.enquiry {
  /* How far the soft halo sits outside the card. Small on purpose: at 390 the
     dialog's own padding is 12px, so anything larger would push the bloom into
     the viewport edge. */
  --enquiry-beam-bleed: 10px;
  /* The crisp lit edge. Started at 2px, which is the same order as the bright
     hairline .enquiry-card__wall already paints (1.5px); rendered at 1440 that
     read as a faint rose line rather than as a beam, and the site's own
     history here is that restraint has been the wrong call every time. 3px on
     a 32px radius still sits inside the 10px refraction band, so it reads as
     the outer edge of the glass catching light rather than as a second
     border. */
  --enquiry-beam-edge: 3px;
  /* One revolution. --dur-ambient (23s) rather than a number of its own: the
     site's ambient family is 19 / 23 / 29s and they are primes so independent
     layers never re-align and pulse together. A colour therefore passes any
     one point on the edge every 5.75s, which is slow enough to read as light
     moving rather than as a spinner. */
  --enquiry-beam-turn: var(--dur-ambient);
}

/* Both layers share the rotating pigment; only the framing differs. */
.enquiry-beam {
  position: absolute;
  pointer-events: none;
}

/* THE SPINNING PIGMENT.

   SIZING, AND WHY IT IS DRIVEN OFF THE HEIGHT. A rotating rectangle only
   covers its parent at every angle if it is at least as big as the parent's
   DIAGONAL on both axes, so this is a square: `height` is definite and
   `aspect-ratio: 1` gives it the matching width. 148% of the card's height
   clears the diagonal for any card up to 1.09 times as wide as it is tall
   (1.48H >= sqrt(W^2 + H^2) <=> W <= 1.09H), and the card measures
   640x757 at 2560, 560x769 at 1440 and 366x750 at 390 -- so the worst real
   case, 2560, has 0.85 against a limit of 1.09. tools/enquiry-beam.mjs
   asserts the covering condition against the card's measured rect rather than
   trusting this arithmetic.

   A CONIC SWEEP RATHER THAN THE REFERENCE'S FOUR CORNER POOLS, AND THIS ONE
   WAS BUILT BOTH WAYS BEFORE IT WAS DECIDED. The reference stacks four
   `radial-gradient(circle farthest-side at <corner>)` pools, which works there
   because its gradient box is the container. Here the box has to be the card's
   DIAGONAL so it can rotate, which is 1138px against a 560x769 card at 1440:
   the whole of the card's perimeter then sits deep inside the square, in the
   region where all four pools overlap, and four overlapping pigments average
   to a flat mauve. Rendered, it read as a grey-pink smudge with no visible
   arc, which is the opposite of the brief.

   A conic gradient's colour is a function of ANGLE only, so it is exactly as
   saturated at the card's edge as it is anywhere else, and the box can be as
   large as rotation needs without diluting anything. The hue sequence is the
   site's own arc -- violet-deep, pink, coral, amber -- closed back onto violet
   at 360deg so the wheel has no seam, and the four alphas differ so the ring
   has brighter and quieter arcs travelling round it rather than one even
   collar of colour. It is a narrow warm arc with no green and no blue in it,
   which is what keeps it a lit edge rather than an RGB halo.

   Every stop carries a real alpha, so none of them is a zero stop and the
   `transparent` trap in the --atmos-* block of tokens.css does not arise here.

   Colours are the tokens' own values, written as rgba because a gradient stop
   needs an alpha: --violet-deep #7b6fd0, --pink #ec5f9c, --coral #f8815d,
   --amber #f7b25f. */
.enquiry-beam__spin {
  position: absolute;
  left: 50%;
  top: 50%;
  height: 148%;
  aspect-ratio: 1;
  transform: translate(-50%, -50%) rotate(0deg);
  background: conic-gradient(from 0deg at 50% 50%,
    rgba(123, 111, 208, 0.86) 0deg,
    rgba(236, 95, 156, 0.72) 92deg,
    rgba(248, 129, 93, 0.60) 176deg,
    rgba(247, 178, 95, 0.82) 264deg,
    rgba(123, 111, 208, 0.86) 360deg);
  animation: enquiry-beam-turn var(--enquiry-beam-turn) linear infinite;
}

/* 0deg and 360deg are the same composition, so a reduced-motion visitor and a
   visitor watching the loop close see the identical frame. That is what makes
   the reduced-motion rule below a one-line `animation: none` rather than a
   parked start state something has to write back. */
@keyframes enquiry-beam-turn {
  from { transform: translate(-50%, -50%) rotate(0deg); }
  to   { transform: translate(-50%, -50%) rotate(360deg); }
}

/* ---------- The halo ----------
   First child of .enquiry-card, so it paints under the refraction band and the
   face and the band genuinely bends it. The blur is on the WRAPPER and the
   clip is on the wrapper too: `overflow` clips the child while the subtree is
   being rendered and the filter runs over that result, so the rounded-rect
   boundary comes out soft. (Do NOT reach for `mask` here instead: filter runs
   BEFORE mask on the same element, so a mask would cut a hard edge back into
   the blurred result. docs/ENGINEERING-NOTES.md, sub-pixel background phase.)

   This is the one per-frame cost in the beam: a blur render surface roughly
   the size of the card, recomposited each frame because its child moves.
   14px, not more, for that reason. Two agents were on the machine when this
   landed, so it has NOT been timed; it needs a quiet-machine confirmation. */
.enquiry-beam--glow {
  inset: calc(var(--enquiry-beam-bleed) * -1);
  border-radius: calc(var(--enquiry-r) + var(--enquiry-beam-bleed));
  overflow: hidden;
  filter: blur(14px);
  opacity: 0.62;
}

/* ---------- The lit edge ----------
   Painted after .enquiry-card__ring and before .enquiry-card__inner, so the
   light sits ON the bevelled wall rather than under it, and every control and
   every focus ring in the form still paints above it.

   The two-layer xor mask is .enquiry-card__rim's own technique: the
   content-box copy punches the interior out and leaves the perimeter band.
   `overflow: hidden` is what keeps the rotating square inside the rounded
   rect; the mask alone would let its corners out. */
.enquiry-beam--edge {
  inset: 0;
  border-radius: inherit;
  overflow: hidden;
  padding: var(--enquiry-beam-edge);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask-composite: exclude;
}

@media (max-width: 560px) {
  /* The card's radius and band come down together at this breakpoint
     (base.css), and the dialog's padding drops to 12px, so the halo comes in
     with them rather than crowding the viewport edge. */
  .enquiry {
    --enquiry-beam-bleed: 7px;
    --enquiry-beam-edge: 2px;
  }
  .enquiry-beam--glow { filter: blur(10px); }
}

/* ---------- Reduced motion ----------
   STILL, BUT STILL THERE. A moving edge around a form is exactly the kind of
   thing this audience needs to be able to stop, and a beam that vanishes
   instead of stopping is a different design, not an accessible one. The
   global rule in base.css already collapses animation-duration to 0.01ms;
   this says it outright so nobody has to reason about which frame that lands
   on, and the base transform is the same composition the loop starts and ends
   at, so nothing is parked anywhere a running animation would have to clear. */
@media (prefers-reduced-motion: reduce) {
  .enquiry-beam__spin { animation: none; }
}

/* Decoration, and a forced palette is the point of this mode. */
@media (forced-colors: active) {
  .enquiry-beam { display: none; }
}
