.container {
  /* The fixed 100px/70px edge insets are a desktop decision — applied unconditionally
     they ate ~45% of a 375px phone's width, wrapping the heading into four cramped
     lines. Comfortable proportional padding below the confinement breakpoint already
     used elsewhere (dot-field, mobile nav wrap), fixed edge insets above it. */
  padding-inline: 1.5rem;
}

@media (min-width: 50.0625rem) {
  .container {
    padding-inline: var(--cb-edge-inset-start) var(--cb-edge-inset-end);
  }
}

/* No border-bottom — the previous 1px Line divider read as an unnecessary
   seam between two surfaces that already read as distinct (Paper header vs
   the hero's own tinted background), and removing it reads calmer and more
   premium/Apple-like. Kept as a plain comment, not a zeroed rule, since
   there's nothing to override — reintroduce a border-bottom here directly
   if a future page ever needs the header to sit on a Paper-on-Paper
   background where the seam would otherwise disappear entirely. */
.site-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--cb-space-4);
  padding-block: 16px; /* down from 20px — a second, further pass toward a slimmer, more precise header */
}

/* Tablet/desktop only — the burger is an edge utility, not part of the
   hero/content grid the wordmark and page copy align to (--cb-edge-inset-
   start, 100px, inherited from .site-header's own .container class). A
   utility like this reads as anchored to the true viewport corner, the way
   a hamburger icon does in a native app chrome, not to a text-column
   inset — so this gives .site-header its own, tighter left gutter instead,
   overriding just padding-left (the .container rule below still governs
   padding-right/inline-end at 90px, untouched). 28px sits mid-range of the
   brief's own 24-32px target.
   Left at the .container-inherited 1.5rem (24px) below 50.0625rem — mobile
   was out of scope for this pass and that value already reads close to
   this same target anyway, so there's nothing to fix there. */
@media (min-width: 50.0625rem) {
  .site-header {
    padding-left: 28px;
  }
}

/* Left group — burger + wordmark. A dedicated wrapper (rather than making
   both direct children of .site-header) so js/nav.js's initNavDrawer can
   inert "everything in the header except the burger button" by targeting
   the wordmark and .site-header__right individually, without needing to
   also reach into this group and exclude the burger by selector. */
.site-header__left {
  display: flex;
  align-items: center;
  gap: 14px; /* down from 20px — reads as one anchored lockup, not two separate floating controls */
}

/* Wordmark alignment — global, every public page. The wordmark's visible
   left edge should align with the main content grid's own left edge
   (--cb-edge-inset-start, the same token .container uses), while the
   burger stays put as its own edge utility (unaffected — this only ever
   nudges the wordmark). The wordmark's natural, unadjusted position (60px)
   comes from unrelated fixed values that aren't expressed as a token
   anywhere (.site-header's own 28px left gutter above, the burger's 40px
   box and -11px edge-alignment margin, and .site-header__left's 14px flex
   gap) — nudging by the measured difference keeps the two aligned if
   --cb-edge-inset-start ever changes, without touching the burger, the
   header's own padding or the right-hand nav. Originally an index.html-only
   inline override; globalised here so every page that shares this header
   inherits the same alignment automatically. */
@media (min-width: 50.0625rem) {
  .site-header .wordmark {
    margin-left: calc(var(--cb-edge-inset-start) - 60px);
  }
}

/* Right group — desktop nav + language selector, kept adjacent to each
   other rather than spread across the remaining header width (the header's
   own justify-content:space-between now only ever separates TWO groups:
   this one and .site-header__left). */
.site-header__right {
  display: flex;
  align-items: center;
  gap: 32px; /* a touch more room now the language control itself is more compact */
}

.site-nav {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 28px; /* down from --cb-space-5 (32px) — a calmer, slightly tighter rhythm */
}

@media (max-width: 40rem) {
  .site-nav {
    gap: var(--cb-space-3);
  }
}

/* Desktop nav links stay hidden below the tablet/desktop split (they remain
   reachable inside the burger drawer instead) — reusing this file's own
   64rem breakpoint, already used elsewhere here as the line between
   "tablet" and "real desktop". Below it, .site-header__right collapses to
   just the language selector. */
@media (max-width: 63.9375rem) {
  .site-nav {
    display: none;
  }
}

/* BURGER BUTTON — plain <button>, three CSS-drawn lines (no icon font, no
   SVG library, no emoji). Generous padding gives a comfortable hit area
   without inflating the visible glyph itself past the brief's 20-24px
   width. Visible on every breakpoint — "supplements the desktop navigation;
   it does not replace it". */
.nav-burger {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  /* The button's own 40px box centres an 18px icon (11px of inset on each
     side) — -11px is what pulls the VISIBLE icon (not just the invisible
     tap-target box) flush against whatever .site-header's own left padding
     currently is, on either side of it cancelling out exactly. That's
     deliberately no longer the shared content-grid inset (100px, still
     used by the wordmark/hero copy/footer) — .site-header now carries its
     own tighter left gutter above (28px at ≥50.0625rem) specifically so
     the burger reads as an edge utility anchored to the true viewport
     corner, not as part of the brand/content grid. This margin value
     doesn't need to change when that gutter does; it always re-aligns the
     icon to whatever padding-left .site-header currently has. */
  margin: -11px;
  padding: 0;
  border: none;
  background: none;
  cursor: pointer;
  /* The drawer panel it opens starts flush with the left edge (left:0,
     same as this button's own position) at every breakpoint, so without
     this the panel would render visually on top of the very button meant
     to close it — position+z-index (one above .nav-drawer's own 200) lifts
     just this button above that stacking context, leaving it reachable
     the whole time the drawer is open. */
  position: relative;
  z-index: 201;
}

.nav-burger__box {
  position: relative;
  width: 18px; /* down from 19px — a further, smaller/lighter pass */
  height: 12px;
}

.nav-burger__line {
  position: absolute;
  left: 0;
  width: 100%;
  height: 1px; /* down from 1.25px — thinner, more restrained */
  background: var(--cb-color-ink);
  transition: transform 280ms var(--cb-ease-standard), opacity 280ms var(--cb-ease-standard),
    top 280ms var(--cb-ease-standard);
}

.nav-burger__line:nth-child(1) {
  top: 0;
}

.nav-burger__line:nth-child(2) {
  top: 50%;
  transform: translateY(-50%);
}

.nav-burger__line:nth-child(3) {
  top: 100%;
  transform: translateY(-100%);
}

/* Open state — the three lines become a restrained X: the outer two meet at
   the vertical centre and rotate, the middle one fades out rather than
   trying to rotate a zero-length line. */
.nav-burger.is-open .nav-burger__line:nth-child(1) {
  top: 50%;
  transform: translateY(-50%) rotate(45deg);
}

.nav-burger.is-open .nav-burger__line:nth-child(2) {
  opacity: 0;
}

.nav-burger.is-open .nav-burger__line:nth-child(3) {
  top: 50%;
  transform: translateY(-50%) rotate(-45deg);
}

@media (prefers-reduced-motion: reduce) {
  .nav-burger__line {
    transition: none;
  }
}

/* NAV DRAWER — same reliable overlay mechanics as .project-drawer/
   .booking-drawer (fixed full-viewport wrapper, [inert] as the single
   source of truth, backdrop + panel as siblings, z-index 200 so it can only
   ever coexist with those if something already went wrong, since JS
   mutually excludes them) — sliding in from the LEFT instead of the right. */
.nav-drawer {
  position: fixed;
  inset: 0;
  z-index: 200;
  visibility: hidden;
  pointer-events: none;
}

.nav-drawer:not([inert]) {
  visibility: visible;
  pointer-events: auto;
}

.nav-drawer__backdrop {
  position: absolute;
  inset: 0;
  background: color-mix(in srgb, var(--cb-color-ink) 35%, transparent);
  opacity: 0;
  transition: opacity 650ms var(--cb-ease-standard);
}

.nav-drawer:not([inert]) .nav-drawer__backdrop {
  opacity: 1;
}

.nav-drawer__panel {
  position: absolute;
  top: 0;
  left: 0;
  width: 100vw;
  height: 100svh;
  overflow-y: auto;
  background: var(--cb-color-paper);
  color: var(--cb-color-ink);
  padding: var(--cb-space-6) var(--cb-space-5);
  outline: none;
  transform: translateX(-100%);
  transition: transform 650ms var(--cb-ease-standard);
  /* A single column, not just a stack of margined blocks — lets
     .nav-drawer__legal pin itself to the true bottom of the panel via
     margin-top: auto (falling back to a plain trailing block, still
     correctly positioned after everything else, on the rare viewport
     short enough that the panel's own overflow-y: auto has to scroll). */
  display: flex;
  flex-direction: column;
}

.nav-drawer:not([inert]) .nav-drawer__panel {
  transform: translateX(0);
}

@media (prefers-reduced-motion: reduce) {
  .nav-drawer__backdrop,
  .nav-drawer__panel,
  .nav-burger__line {
    transition: none;
  }
}

@media (min-width: 50.0625rem) {
  .nav-drawer__panel {
    width: 65vw; /* tablet — within the brief's 60-70vw */
    border-right: 1px solid var(--cb-color-line);
  }
}

@media (min-width: 64rem) {
  .nav-drawer__panel {
    width: 450px; /* desktop — within the brief's 420-480px */
  }
}

.nav-drawer__primary {
  display: flex;
  flex-direction: column;
  gap: var(--cb-space-4);
  margin-top: var(--cb-space-8);
}

/* Refinement pass: down from the page-heading clamp (32-40px) to an
   explicit 26/27/28px tier — still clearly the largest, most prominent
   text in the drawer, just no longer competing with an actual H1. */
.nav-drawer__link {
  display: inline-block;
  width: fit-content;
  font-family: var(--cb-font-sans);
  font-weight: var(--cb-weight-semibold);
  font-size: 26px;
  line-height: 1.2;
  color: var(--cb-color-ink);
  text-decoration: none;
  background-image: linear-gradient(currentColor, currentColor);
  background-position: 0 100%;
  background-repeat: no-repeat;
  background-size: 0% 1px;
  transition: background-size 280ms var(--cb-ease-standard);
}

@media (min-width: 50.0625rem) {
  .nav-drawer__link {
    font-size: 27px;
  }
}

@media (min-width: 64rem) {
  .nav-drawer__link {
    font-size: 28px;
  }
}

.nav-drawer__link.is-placeholder {
  cursor: default;
  color: var(--cb-color-text-secondary);
}

.nav-drawer__link:not(.is-placeholder):hover,
.nav-drawer__link:not(.is-placeholder):focus-visible {
  background-size: 100% 1px;
}

/* Current-page indicator (Contact, only while on contact.html) — a
   permanent thin underline, distinct from the hover/focus-revealed one
   every other link here uses, so it can't be mistaken for a hover state. */
.nav-drawer__link[aria-current="page"] {
  background-size: 100% 1px;
}

/* ACTIONS — the drawer's one divider, then Book a meeting / Core login /
   email / Instagram as a single flat list of secondary action links (no
   "Connect" eyebrow grouping them any more — one plain list reads calmer
   and avoids the drawer turning into a labelled sitemap). */
.nav-drawer__actions {
  margin-top: var(--cb-space-8);
  padding-top: var(--cb-space-6);
  border-top: 1px solid var(--cb-color-line);
  display: flex;
  flex-direction: column;
  gap: var(--cb-space-3);
}

.nav-drawer__action-link {
  display: block;
  width: fit-content;
  font-family: var(--cb-font-sans);
  font-weight: var(--cb-weight-medium);
  font-size: var(--cb-text-body-size);
  color: var(--cb-color-ink);
  text-decoration: none;
  background-image: linear-gradient(currentColor, currentColor);
  background-position: 0 100%;
  background-repeat: no-repeat;
  background-size: 0% 1px;
  transition: background-size 280ms var(--cb-ease-standard);
}

.nav-drawer__action-link.is-placeholder {
  cursor: default;
  color: var(--cb-color-text-secondary);
}

.nav-drawer__action-link:not(.is-placeholder):hover,
.nav-drawer__action-link:not(.is-placeholder):focus-visible {
  background-size: 100% 1px;
}

/* LEGAL — pinned to the true bottom of the panel (see .nav-drawer__panel's
   own flex-column comment), small and quiet: Privacy / Terms / Cookie
   preferences. Plain text, no underline language — these are utility
   links, not primary or secondary navigation. */
.nav-drawer__legal {
  margin-top: auto;
  padding-top: var(--cb-space-6);
  display: flex;
  flex-wrap: wrap;
  gap: var(--cb-space-2) var(--cb-space-4);
}

.nav-drawer__legal a,
.nav-drawer__legal button {
  appearance: none;
  margin: 0;
  padding: 0;
  border: none;
  background: none;
  font-family: var(--cb-font-sans);
  font-size: var(--cb-text-small-size);
  color: var(--cb-color-text-secondary);
  text-decoration: none;
  cursor: pointer;
  transition: color 200ms var(--cb-ease-standard);
}

.nav-drawer__legal a:hover,
.nav-drawer__legal a:focus-visible,
.nav-drawer__legal button:hover,
.nav-drawer__legal button:focus-visible {
  color: var(--cb-color-ink);
}

@media (prefers-reduced-motion: reduce) {
  .nav-drawer__link,
  .nav-drawer__action-link,
  .nav-drawer__legal a,
  .nav-drawer__legal button {
    transition: none;
  }
}

/* Client/account icon — the only control left in .site-header__right
   besides the primary nav/burger (the language selector that used to sit
   here has been removed sitewide — single-language site). Sized and
   weighted to match the language selector's globe icon exactly (15px,
   stroke-width 1.5, Graphite via currentColor) rather than introducing a
   new icon scale. cursor: default (not pointer) and no hover treatment at
   all — same restrained "visible but not yet actionable" convention as
   .cta--capabilities elsewhere, since this doesn't navigate anywhere until
   a real client portal exists. */
.header-account {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 8px;
  margin: -8px;
  border: none;
  background: none;
  color: var(--cb-color-text-secondary);
  /* Now a real <a href="login.html">, not the earlier non-interactive
     "coming soon" control — cursor updated to match (was `default`). */
  cursor: pointer;
  text-decoration: none;
}

.header-account__icon {
  /* Sized up from an earlier 15px pass — that read as visibly weaker/
     smaller than the "EN" label beside it. 20px brings its optical size
     close to that label's own visual height, matching the brief's
     "20-22px" target. stroke-width on the SVG itself was reduced from 1.5
     to 1.15 (in expertise.html/index.html/etc.) to compensate for the
     larger rendered box — without that, the same viewBox-relative stroke
     would render visibly thicker than the globe icon at this bigger size;
     1.15 keeps the actual on-screen line weight equal to (if anything
     marginally thinner than) the globe's own. */
  width: 20px;
  height: 20px;
  flex-shrink: 0;
  /* Flexbox centres this icon's box exactly, but the glyph's own drawn
     weight (the head circle + shoulders arc sit slightly above the
     geometric centre of the 24-unit viewBox) reads as optically low next
     to "EN"/the globe/Contact. A small icon-only nudge, not a change to
     .header-account's own flex alignment. */
  transform: translateY(-1.5px);
}

.hero {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: center;
  min-height: min(44rem, 88vh);
  padding-block: var(--cb-space-7);
  overflow: hidden;
}

/* Desktop only — keeps the hero tall enough that the header + hero fill the
   entire initial viewport, so no part of .capabilities can appear above the
   fold on a normal scroll-free load. justify-content:center (above) ties
   the content's vertical position to the section's total height: simply
   raising min-height here would have recentred the content into the extra
   room, shifting the headline down and violating "preserve the current
   position of the hero headline" — so this switches to a fixed padding-top
   instead, pinned to 215.546875px, the exact offset the base rule's own
   centring already produces at min-height's 704px cap (confirmed by
   measurement, unchanged from before this rule existed) — then
   justify-content:flex-start so any extra height this min-height adds
   becomes trailing space AFTER the content, never redistributed above it.
   min-height (a floor), not height, so content already taller than the
   viewport on an unusually short screen still can't be clipped. The
   subtraction matches the header's own real rendered height exactly
   (2×--cb-space-4 padding-block + one line of --cb-text-body-size at
   --cb-text-body-leading, the nav links' own font metrics) rather than a
   guessed constant, so it can't drift out of sync if either ever changes. */
@media (min-width: 64rem) {
  .hero {
    padding-top: 215.546875px;
    justify-content: flex-start;
    min-height: calc(
      100vh - (2 * var(--cb-space-4)) - (var(--cb-text-body-size) * var(--cb-text-body-leading))
    );
  }
}

.hero__dot-field {
  position: absolute;
  /* A restrained clear-space zone above the first dots, now that there's no
     header divider to do that separation job visually — the field itself
     is absolutely positioned inside .hero (position:relative), so shifting
     its own top edge down is fully independent of .hero's own padding-top
     (which governs the heading/copy/CTA's position instead): the dots move,
     nothing else does. js/dot-field.js reads this element's own rendered
     clientHeight every time it draws, so a shorter box here simply means
     one fewer row is generated at the top — the same spacing/size/falloff
     formulas, not a different composition — and the existing top-edge fade
     (css/dot-field.css's own mask-image) recalculates against this new,
     shorter box automatically, so the first dots still fade in softly
     rather than starting with a hard edge. 24px here is the smaller mobile/
     narrow-tablet default; the ≥50.0625rem tier below raises it to 44px. */
  top: 24px;
  bottom: 0;
  right: 0;
  /* `.hero` is now full-bleed (see .hero__content below), so right:0 is the true
     viewport edge — the field runs all the way to it and the mask in
     css/dot-field.css only fades the left edge, so the pattern reads as continuing
     past the visible area rather than politely stopping before it. */
  width: 42%;
  z-index: 0;
}

/* Aligned with .hero's own real desktop switch point (64rem, above) rather
   than an earlier, mismatched 50.0625rem — that gap meant genuine tablet
   widths (e.g. 820px) fell into this "fixed 620px left edge" desktop mode
   too early, producing a cropped, awkward-looking narrow vertical strip
   (confirmed by screenshot) instead of the intentional confined mobile/
   tablet composition below. Below 64rem now stays in the confined
   composition through the whole mobile+tablet range. */
@media (min-width: 64.0625rem) {
  .hero__dot-field {
    /* Above the mobile/tablet confinement breakpoint, anchor the left edge to a fixed
       point instead of a viewport percentage: a percentage width made the overlap with
       the heading inconsistent (barely touching at 1440px, a 300px gap at 1920px, since
       the heading's own max-width:20ch caps its growth while a %-width field keeps
       growing). A fixed left edge gives the same "slightly overlapping, fading"
       relationship to the (also fixed-cap) heading at any wide viewport. */
    left: 620px;
    width: auto;
    /* Up from the 24px mobile/tablet default — within the brief's own 36-56px
       target for the gap beneath the (now divider-less) header. */
    top: 44px;
  }
}

.hero__content {
  position: relative;
  z-index: 1;
  /* Flex items default to min-width: auto, which lets long inline content (a heading with
     a wide italic word) refuse to wrap and overflow the column instead. */
  min-width: 0;
}

.hero__heading {
  max-width: 20ch;
}

.hero__supporting {
  margin-top: var(--cb-space-4);
  max-width: var(--cb-measure-body);
  color: var(--cb-color-text-secondary);
}

.hero__cta-group {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--cb-space-4);
  /* Down from space-6 (48px) — that gap was tuned for the old filled-button CTA;
     the slim text-link replacement (see components.css's .cta) reads too far
     detached from the paragraph above it at that spacing. */
  margin-top: var(--cb-space-4);
}

.capabilities {
  position: relative;
}

.capabilities__grid {
  position: relative;
  z-index: 1;
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--cb-space-7);
  /* Sitewide section-to-section gap is 2×var(--cb-section-gap) (tokens.css)
     — every boundary shares the same figure, each side contributing one
     half. hero->capabilities is the one boundary that can't actually reach
     it: hero's own min-height + justify-content:center already puts
     ~215.56px of dead space beneath its content at the 1440x900 reference
     viewport (unchanged — hero's own internal sizing is out of scope here,
     "do not reduce internal spacing inside each section"), which alone
     already exceeds the new, smaller desktop target (192px total). No
     amount of padding here can close that gap without shrinking to a
     negative value, so this is set to 0 — the smallest this side can
     usefully contribute — and the total gap on this one boundary is simply
     whatever hero's own leftover happens to be at a given viewport height,
     not the shared 2×var(--cb-section-gap) every other boundary hits
     exactly. Flagged, not silently absorbed into the shared token. */
  padding-top: 0;
  padding-bottom: var(--cb-section-gap);
}

/* Raised from 50.0625rem to 64.0625rem (same tablet/desktop realignment as
   .hero__dot-field above): splitting into two columns at ~800px halved
   .capabilities__visual's own available width at the exact same breakpoint
   --capability-circle-size (below) also jumps to its largest 7.5rem tier —
   confirmed by screenshot to badly overlap/clip the 10-circle ring at
   820px. Below 64rem now stays single-column/full-bleed, the same
   composition already verified circle-overlap-safe down to 375px, so it
   has more than enough room at genuine tablet widths. */
@media (min-width: 64.0625rem) {
  .capabilities__grid {
    grid-template-columns: 1fr 1fr;
    align-items: center;
    gap: var(--cb-space-8);
  }
}

.capabilities__visual {
  display: flex;
  align-items: center;
  justify-content: center;
  /* Full-bleed below the two-column breakpoint: with 10 circles at a slightly bigger
     size than before, the container's own padded width (100% of .container, inset
     1.5rem each side) isn't enough room to keep them from overlapping on the
     smallest required phone widths (375/390/412px) — worked through the same
     chord-length math as --capability-circle-size's comment below. Breaking out to
     the true viewport edge recovers exactly that 3rem (1.5rem x2) without having to
     shrink the circles back down. Purely decorative and clipped (see overflow below),
     so there's no harm in it running edge-to-edge. */
  width: 100vw;
  margin-inline: calc(50% - 50vw);
  /* Must reserve the ring's full rendered span (orbit + circle bleed on both sides),
     which .capability-ring's own width/max-width formula works out to exactly
     min(container width, 36rem) — see the "BASE" comment on .capability-ring's
     max-width. Falling short here doesn't cause horizontal overflow (that's the
     overflow:hidden below), it causes vertical clipping instead: found by screenshot
     at 768px after the ring first grew for 9 circles — a fixed min-height well under
     what the bigger orbit needed there clipped whichever circles rotated nearest the
     top/bottom edge at any given moment. 100vw, not calc(100vw - Nrem), now that this
     element is itself full-bleed (see width above) — the ring's own container is the
     true viewport width, no page padding to subtract back out.

     37rem, not the ring's own 36rem, here specifically — .capability-ring's max-width
     already reserves exactly enough room for a hovered circle's scaled-up size (see
     --capability-hover-scale), leaving mathematically zero slack between the ring's
     total rendered span and this container's edge. That exact-fit boundary clipped a
     hovered circle intermittently in testing (sub-pixel rounding tips it either side
     of the line) — this 1rem is pure headroom, not part of the ring/circle spacing
     math, so it doesn't touch overlap safety between circles at all. */
  min-height: min(37rem, 100vw);
  overflow: hidden;
}

/* Kept aligned with .capabilities__grid's own two-column breakpoint above
   (64.0625rem) — this element must stay full-bleed for as long as the grid
   itself stays single-column, or it would shrink back to a half-width
   column while still needing the full viewport width for its 10-circle
   ring. */
@media (min-width: 64.0625rem) {
  .capabilities__visual {
    /* Above this breakpoint the layout goes two-column and this element must go
       back to respecting its own grid column instead of the viewport. */
    width: 100%;
    margin-inline: 0;
    /* .capability-ring's max-width is always the binding constraint at this size
       regardless of exactly how wide the column is, so the span is simply 36rem —
       plus the same 1rem hover-clipping safety margin as above. */
    min-height: 37rem;
  }
}

/* A constantly-rotating ring of circles, each with a real, always-visible label.
   Rotation is driven entirely by js/main.js via requestAnimationFrame — there is no
   CSS @keyframes animation anywhere in this component. That's a deliberate rewrite,
   not the original design: an earlier CSS-only version (.track spinning via
   @keyframes, each .circle counter-rotating via its OWN @keyframes to stay upright)
   fought JS-driven hover changes at the architecture level. An active CSS animation
   always takes priority over `transition` on the same property of the same element,
   so every attempt to smoothly ease a hover-triggered change (extra angular spacing,
   scale) was competing with an animation that already owned `transform` — the
   change could only ever apply as a hard, un-eased cut. No amount of tuning
   durations/easing curves fixes that; the property has to not be animation-owned in
   the first place. Now: .capability-ring__item's rotation and
   .capability-ring__circle's counter-rotation are both plain, JS-written inline
   styles (no animation on either), so ordinary interpolation (done by hand in the
   rAF loop, see main.js) is the only thing ever touching them. Hover SCALE lives on
   a further-nested .capability-ring__circle-visual specifically so it never shares a
   `transform` with orbit positioning at all — see that rule's own comment. */
.capability-ring {
  /* A circle's CENTRE sits on the ring's own edge (see .capability-ring__circle's
     comment below), so half of every circle's own diameter extends past the ring's
     bounding box in every direction — a plain width:100% caused real horizontal page
     overflow on mobile (confirmed via document.documentElement.scrollWidth, not just a
     screenshot guess) because that overflow was never subtracted back out. Reserving
     --capability-circle-size on both axes leaves exactly enough clearance regardless of
     breakpoint, instead of hardcoding a magic-number gap that would drift out of sync
     with the circle's own responsive size.

     --capability-circle-size's three tiers are all bigger than the previous 4.75rem/
     6rem/6.75rem set (per "make the circles a bit bigger"), made possible by
     .capabilities__visual going full-bleed below the two-column breakpoint (see its
     own comment) rather than by shrinking anything: 10 circles spaced evenly (36deg
     apart) need each pair of neighbouring centres to sit further apart than the
     circles are wide, or they overlap, and that math is tighter with 10 circles than
     the previous 9. Checked by hand against the required 375/390/412px phone widths
     with the full-bleed container and verified with a scripted per-circle
     bounding-box check across a full 32s rotation (no clipping, no overlap).

     The clearance reserved on each axis is circle-size * hover-scale, not just
     circle-size — a hovered circle visually grows past its own normal box (see
     --capability-hover-scale, read by components.css's hover/focus rule too, so the
     two stay in sync automatically instead of as two hand-matched numbers), and
     without the extra headroom that growth got clipped by this element's own
     overflow:hidden whenever the hovered circle was near the top or bottom of the
     ring (reported after "make the circles even bigger when hovering" made the old,
     smaller headroom insufficient). This works out to slightly less orbit spacing
     between circles than reserving plain circle-size would, but still comfortably
     clear of overlap at every required viewport (re-verified by script). */
  --capability-circle-size: 5rem;
  /* 1.09, then 1.15, both still read as too subtle a grow against these
     circle sizes once live. 1.25 is the corrected target. This is the one
     value both components.css's hover rule and the clearance calc below
     read, so the reserved headroom for a hovered circle stays in sync
     automatically. */
  --capability-hover-scale: 1.25;
  /* Hover colour hierarchy. Scoped here rather than added to tokens.css
     since they're specific to this one component's interaction states, not
     a new sitewide palette entry. Consumed in components.css, same
     cross-file pattern already used for the two tokens above. Only two
     states (inactive light-grey / active ink) — the previous "dimmed" state
     for non-hovered circles during interaction is gone; both are the same
     colour, so there's nothing left to distinguish.
     --capability-circle-bg reuses .feature-panel__visual's own background
     exactly (the featured-project card's right-side, slightly darker
     colour — the left panel's own 2% tint read as too light once live) —
     not a new, approximate hex, per brief. --capability-circle-text is
     unchanged, a literal hex from the earlier colour brief (kept as-is, not
     tied to a token, since nothing asked for it to be).
     --capability-circle-bg-active/-text-active are the design brief's own
     literal hex values. */
  --capability-circle-bg: color-mix(in srgb, var(--cb-color-ink) 8%, var(--cb-color-paper));
  --capability-circle-text: #4f4d49;
  --capability-circle-bg-active: #1c1c1a;
  --capability-circle-text-active: #faf9f5;
  position: relative;
  width: calc(100% - (var(--capability-circle-size) * var(--capability-hover-scale)));
  /* Base bumped 30rem -> 36rem alongside the circle-size increase, so the orbit
     keeps the same proportion of clearance around the now-bigger, now-more-numerous
     circles rather than just getting relatively tighter. */
  max-width: calc(36rem - (var(--capability-circle-size) * var(--capability-hover-scale)));
  aspect-ratio: 1;
  margin-inline: auto;
}

@media (min-width: 30rem) {
  .capability-ring {
    --capability-circle-size: 6.5rem;
  }
}

/* Aligned with .capabilities__grid's own 64.0625rem two-column breakpoint
   above — the largest circle tier is reserved for where the ring also has
   the full two-column desktop width to work with; the 30rem tier below
   (6.5rem circles, full-bleed) now correctly covers the whole tablet range
   too. */
@media (min-width: 64.0625rem) {
  .capability-ring {
    --capability-circle-size: 7.5rem;
  }
}

.capability-ring__track {
  /* No longer animated — kept purely as a structural wrapper (removing it would mean
     reordering/flattening the DOM, which wasn't asked for). Each item now carries
     its own full, already-composed rotation (orbit spin + hover redistribution)
     directly, written by js/main.js every frame. */
  position: absolute;
  inset: 0;
}

/* Sibling of .track, not a child — it must stay put and upright regardless of the
   ring's rotation, so it sits directly on .capability-ring instead of inheriting the
   spin. pointer-events:none so it never intercepts a hover meant for a circle
   passing behind/near it. */
.capability-ring__center {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--cb-space-4);
  pointer-events: none;
}

/* One statement, one <p>, two inline spans — not a title plus a body. The
   capability name is never shown here (see index.html's comment); only the
   explanatory line, swapped via js/main.js's setCenterContent, which toggles
   .is-changing to fade the text out, swaps its content while invisible, then
   removes the class to fade the new statement in — see that function's own
   comment for why a token check, not a cancelled timer, guards rapid switching.
   opacity/transform are the only animated properties (never `all`), per brief.

   Sizing is ONE shared rule for every state, default and hover alike — only
   the text CONTENT changes between them, never the typography. An earlier
   pass gave `.is-default` its own separate, smaller font-size/max-width
   (reasoning: the ~17-word default sentence is roughly twice the length of
   any single hover statement, so it needed more room to avoid overlapping
   the circles). That produced a visible size jump on every hover/unhover,
   which is the regression this rule now avoids: the values below are sized
   for the LONGEST content (the default statement), which is a strictly
   tighter constraint than any shorter hover statement, so every hover state
   automatically has equal or more clearance at the same font-size — nothing
   needs its own tier. .is-default is not used here at all; it exists purely
   as a content-transition hook (see js/main.js) and must never gain a
   typography rule again. */
.capability-ring__center-text {
  text-align: center;
  color: var(--cb-color-text);
  opacity: 1;
  transform: translateY(0);
  transition: opacity 180ms var(--cb-ease-standard), transform 180ms var(--cb-ease-standard);

  /* Sized in three tiers, not one clamp() everywhere — the widest desktop
     value is explicitly a "desktop target"; applied below 50.0625rem it
     would be wildly oversized for a phone-width ring. There's also a THIRD,
     in-between tier: between the two-column breakpoint (50.0625rem/801px)
     and roughly 75rem/1200px, the two-column grid leaves this ring's own
     column narrower than either a typical laptop width above that or the
     single-column layout below it (documented in an earlier pass — the ring
     itself measures as little as ~219px around 1024px specifically, before
     growing again past 1200px). max-width is a PERCENTAGE, not a ch/px
     value, so it scales down together with the ring's own current rendered
     size automatically rather than needing a value per breakpoint. */
  font-size: clamp(0.75rem, 2.85vw, 0.95rem);
  line-height: 1.15;
  max-width: 65%;
}

/* FINAL REVIEW (F-2): .capabilities__grid's own two-column breakpoint is now
   64.0625rem (raised from the old 50.0625rem in an earlier pass — see that
   rule's comment), so the whole 50.0625rem–64rem slice of what used to be one
   "squeezed two-column" tier is actually SINGLE-COLUMN, full-bleed, with the
   ring at its large 6.5rem circle-size — genuinely more room than the old
   clamp(0.45rem, 0.7vw, 0.6rem) (a leftover sized for the narrow two-column
   squeeze) assumed, which is why the statement read as illegibly small at
   768/820 even though the ring itself now fits. Split into its own tier with
   real, legible sizing — still deliberately smaller than the circle labels'
   fixed 0.875rem (--cb-text-small-size), so hierarchy stays label > centre
   statement, just no longer illegible. */
@media (min-width: 50.0625rem) and (max-width: 64rem) {
  .capability-ring__center-text {
    font-size: clamp(0.7rem, 1.3vw, 0.8125rem);
    line-height: 1.3;
    max-width: 60%;
  }
}

/* Genuine two-column squeeze range (64.0625rem–74.9375rem) — the ring here is
   still as narrow as the original measurement found, so this keeps the
   original tiny, overlap-safe sizing unchanged. */
@media (min-width: 64.0625rem) and (max-width: 74.9375rem) {
  .capability-ring__center-text {
    font-size: clamp(0.45rem, 0.7vw, 0.6rem);
    max-width: 40%;
  }
}

@media (min-width: 75rem) {
  .capability-ring__center-text {
    font-size: clamp(1rem, 1.3vw, 1.3rem);
    line-height: 1.25;
    max-width: 68%;
  }
}

.capability-ring__center-text.is-changing {
  opacity: 0;
  transform: translateY(0.35rem);
}

.capability-ring__center-text-light {
  font-weight: var(--cb-weight-light);
}

.capability-ring__center-text-emphasis {
  font-weight: var(--cb-weight-semibold);
}

.capability-ring__item {
  position: absolute;
  inset: 0;
  /* Every item's box is the ring's FULL size (inset:0), not just the circle's own
     small area — only .capability-ring__circle inside it is actually positioned at
     the edge. With 10 stacked full-size boxes, whichever item is later in DOM order
     sits on top and silently intercepts hover/click meant for an earlier item's
     circle underneath it (caught by Playwright: "element intercepts pointer events",
     not visible by eye since these boxes have no background). Turning pointer events
     back on at the circle itself (below) is enough — nothing else in here needs them. */
  pointer-events: none;
  /* --angle is written directly by js/main.js every animation frame — it is ALREADY
     the fully composed value (this item's fixed position in the 10-item sequence +
     the ring's current orbit rotation + this item's current, eased hover-
     redistribution offset), not a base value some other layer adds to. There is
     nothing else touching `transform` on this element — no @keyframes, no
     transition — so whatever the rAF loop writes is exactly what renders, with no
     competing owner of the property. */
  transform: rotate(var(--angle, 0deg));
}

.capability-ring__circle {
  position: absolute;
  /* Centred on the item box's own top edge — after the item's rotate(), that point
     sweeps to the correct position on the ring's circumference. Orbit radius is
     therefore always exactly half of .capability-ring's own rendered size, not a fixed
     px/rem value, so it scales correctly at any container width without a breakpoint
     table — only the circle's own size (--capability-circle-size) needs one. */
  top: 0;
  left: 50%;
  /* Positioning + counter-rotation ONLY — no scale here. --counter-angle is written
     by the same rAF loop as .capability-ring__item's --angle, in the same frame, so
     the two are always perfectly in sync (this element's rotation always exactly
     cancels the item's, keeping the label upright, with no separate timing to drift
     out of step). Scale lives on .capability-ring__circle-visual, one level deeper —
     see that rule's comment for why it's not folded in here. */
  transform: translate(-50%, -50%) rotate(var(--counter-angle, 0deg));
  width: var(--capability-circle-size);
  height: var(--capability-circle-size);
}

/* Selected work — now a single compact, one-viewport feature (brief:
   "should fit within approximately one desktop viewport... do not create
   multiple viewport-height blocks"), replacing a much longer previous
   implementation entirely. min-height (not height) so real content is
   never clipped if it ever needs more room than 92svh provides;
   justify-content:center balances the (shorter-than-92svh) content
   vertically within that floor on taller viewports rather than leaving it
   pinned to the top with dead space below. */
.selected-work {
  position: relative;
  min-height: 92svh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  /* This section's half of the shared 2×var(--cb-section-gap) section-to-
     section gap (capabilities' own padding-bottom is the other half). No
     padding-bottom here — the gap to .process is owned entirely by
     .feature-panel-outer's own margin-bottom below, for the same reason:
     a separate padding-bottom here would silently stack on top of it. */
  padding-top: var(--cb-section-gap);
}

.selected-work__intro {
  /* Centred per brief. 45rem (720px) is inside "approximately 680-740px". */
  max-width: 45rem;
  margin-inline: auto;
  text-align: center;
}

/* Outer wrapper — centres + caps the panel at the Apple reference's own
   measured 1260px max-width. Deliberately its own thing, not the sitewide
   .container class: .container's fixed 100/90px insets have no upper cap,
   which is exactly why the previous pass kept stretching wider than 1260px
   on large monitors. width:calc(100% - 2×margin), not padding-inline on a
   box that also carries a background — this wrapper is purely a centring
   shell, so its "margin" needs to shrink the box itself, not pad inward
   around a visible surface (.feature-panel, one level in, is what actually
   paints). Three margin tiers matched 1:1 to the brief's own three named
   breakpoints below; the desktop tier's 90px/side lands the wrapper at
   exactly 1260px at 1440px wide (1440 − 180 = 1260), confirmed by
   measurement, with the max-width taking over as the hard ceiling beyond
   that. */
.feature-panel-outer {
  width: calc(100% - 48px);
  max-width: 1260px;
  margin-inline: auto;
  /* Intro-to-card gap, set here on the card wrapper rather than as padding
     on .feature-panel itself. Unrelated to the sitewide section-to-section
     rhythm below — this is spacing within the Selected Work section, not
     between sections. */
  margin-top: clamp(64px, 6vw, 96px);
  /* Card-to-next-section gap: this section's half of the shared
     2×var(--cb-section-gap) section-to-section gap (.process's own
     margin-top is the other half). Not a clamp — this now tracks the same
     token-driven, breakpoint-tiered value every other boundary uses, so it
     can't drift out of sync with them at any viewport width. */
  margin-bottom: var(--cb-section-gap);
}

@media (min-width: 50.0625rem) {
  .feature-panel-outer {
    width: calc(100% - 96px);
  }
}

@media (min-width: 80rem) {
  .feature-panel-outer {
    width: calc(100% - 180px);
  }
}

/* The feature panel — one unified rounded panel with no padding of its own
   (brief: "do not add outer padding to the entire panel"); the two columns
   below carry their own insets instead. The intro-to-card and card-to-next-
   section gaps live on .feature-panel-outer instead (the "card wrapper"),
   not here — brief: "not by adding internal padding to the card". */
.feature-panel {
  position: relative;
  width: 100%;
  border-radius: var(--cb-radius-panel);
  /* Very pale Ink-tinted Paper — the existing subtle neutral surface,
     unchanged from the previous pass. No border, no shadow — brief: "no
     heavy shadow... no additional border around the visual". */
  background: color-mix(in srgb, var(--cb-color-ink) 2%, var(--cb-color-paper));
  overflow: hidden;
}

/* Inner wrapper carries the one-time arrival transform (js/main.js's
   initFeaturePanelReveal) — .feature-panel itself never resizes/moves, only
   this wrapper's content settles into place. It's also the actual flex row
   the brief specifies (row/nowrap/align-items:center) — kept as a separate
   element from .feature-panel rather than merging the two, purely so the
   arrival transform has something to animate without moving the panel's
   own overflow:hidden/radius box. Mobile-first: stacked column, height
   auto (brief: "do not force the 480px desktop height" on mobile); the
   ≥50.0625rem tier below brings the two side by side at a fixed height. */
.feature-panel__reveal {
  display: flex;
  flex-direction: column;
  height: 100%;
  transform: scale(1) translateY(0);
  transition: transform 900ms var(--cb-ease-standard);
}

/* .feature-panel__content is the LEFT REGION: full column height,
   vertically centring whatever compact copy block sits inside it — brief:
   "should occupy the complete 480px panel height, but its inner text block
   should remain compact and vertically centred". The actual padding/inset
   lives one level deeper, on .feature-panel__copy, so this element's only
   job is the centring. */
.feature-panel__content {
  display: flex;
  flex-direction: column;
  align-items: center;
  /* justify-content, not align-items, is what centres content along a
     column flex's main (vertical) axis — align-items only ever controlled
     the cross (horizontal) axis here, so the copy block was actually
     top-aligned despite the comment above's intent, sitting closer to the
     bottom of the 480px column once its own content grew taller. */
  justify-content: center;
  padding: var(--cb-space-5) 0;
}

/* The "inner copy wrapper" the brief explicitly asks for — label, title,
   paragraph and CTA all live inside this one compact block, not directly
   in .feature-panel__content, so the padding constraints below apply to the
   text block alone and never stretch to fill the full 480px column.
   No max-height here any more — it previously capped this box at 230px
   while the actual content (once the description grew to its current,
   longer copy) needed closer to 325px, so the content silently overflowed
   past the box's bottom edge. .feature-panel__content's justify-content:
   center only ever centres THIS box, not whatever spills out of it, so the
   overflow (always downward, never upward, since document flow only grows
   down) was the actual cause of the copy block visually sitting low with
   the CTA crowded against the bottom — confirmed by measuring the true
   content (label top to CTA bottom) separately from this box's own
   (max-height-capped) rect. Letting the box size to its real content is
   what makes the centring correct instead of just present in the CSS. */
.feature-panel__copy {
  width: 100%;
  padding: 0 var(--cb-space-4) 0 var(--cb-space-4);
  text-align: left;
}

.feature-panel__title {
  /* 24px sits inside the brief's own "label to title: approximately
     22-28px"; the paragraph below carries its own top margin instead of
     this rule's bottom margin, so "title to paragraph" (24-30px) is set in
     exactly one place, not split across two rules. */
  margin: 24px 0 0;
  font-family: var(--cb-font-sans);
  font-weight: var(--cb-weight-semibold);
  /* Mobile: kept from the previous pass — brief's own "38-44px" range is
     scoped to desktop only this time; mobile isn't restated, so the
     already-tuned "28-32px" mobile size (and the reasoning that the title
     must read as clearly smaller than the main Selected Work H2, which
     caps at 40px) carries forward unchanged. */
  font-size: clamp(1.75rem, 1.6rem + 0.7vw, 2rem);
  line-height: 1.2;
  color: var(--cb-color-ink);
}

.feature-panel__description {
  /* 26px sits inside the brief's own "title to paragraph: approximately
     24-30px". */
  margin: 26px 0 0;
  font-family: var(--cb-font-sans);
  font-weight: var(--cb-weight-regular);
  /* Brief's own "approximately 17-19px"; wraps within
     .feature-panel__copy's own width (set by the column/padding below),
     not a separate max-width here, so it stays correct at every tier
     without needing its own breakpoint overrides. */
  font-size: clamp(1.0625rem, 1rem + 0.25vw, 1.1875rem);
  line-height: 1.55;
  color: var(--cb-color-text-secondary);
}

/* Two-class selector, not just .feature-panel__cta: components.css's own
   shared .cta rule sets `margin: -0.5em -0.25em` (the transparent-padding/
   negative-margin bigger-tap-target technique) as a single-class selector
   loaded AFTER this stylesheet, so at equal specificity it was silently
   winning and overriding margin-top back to roughly -8.5px, no matter what
   value was set here — confirmed via computed style. This compound
   selector's higher specificity fixes that without touching the shared
   .cta rule or its other three sides' tap-target margins. */
.feature-panel__copy .feature-panel__cta {
  /* Matches .hero__cta-group's own text-to-CTA gap (space-4 = 24px) — the
     sitewide convention for a slim .cta text-link sitting right under a
     paragraph; a wider gap reads as too detached from the copy above it. */
  margin-top: var(--cb-space-4);
}

/* RIGHT REGION — brief: "fills the full right column... begin immediately
   where the left column ends... touch the top, bottom and right edges of
   the feature panel... not sit inside another rectangle... not have
   padding around it". No padding, no border, no aspect-ratio box of its
   own on the ≥50.0625rem tiers below — the column's own flex-basis is what
   gives it its size, and .feature-panel's own overflow:hidden clips its
   outer corners to the panel radius automatically.
   The 8% ink tint is no longer just placeholder filler — the supplied
   composite (.feature-panel__image below) is a transparent PNG collage of
   overlapping screenshots, not an opaque rectangular photo, so this tint
   is what actually shows through the gaps around it once rendered. */
.feature-panel__visual {
  display: flex;
  align-items: center;
  justify-content: center;
  aspect-ratio: 4 / 3;
  background: color-mix(in srgb, var(--cb-color-ink) 8%, var(--cb-color-paper));
  text-align: center;
}

/* object-fit:contain, not cover — the supplied composite
   (assets/images/les-cles-de-charlotte-feature.png) is a multi-panel
   collage of overlapping desktop/mobile/brochure screenshots on a
   transparent background, not a single rectangular photo; cropping it via
   cover would cut the overlapping panels off arbitrarily. */
.feature-panel__image {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
}

@media (min-width: 50.0625rem) {
  .feature-panel {
    /* Tablet — brief's own "reduce panel height proportionally" from the
       480px large-desktop figure. */
    height: 420px;
  }

  .feature-panel__reveal {
    flex-direction: row;
    flex-wrap: nowrap;
    /* No align-items override — default `stretch` is what's needed here.
       A previous `align-items: center` shrank .feature-panel__content down
       to hug its own content height instead of filling the row, which left
       that element's own justify-content:center with no spare height to
       centre anything within — it was a no-op riding on this rule doing the
       real (visually correct, but structurally wrong) centring instead.
       .feature-panel__visual is unaffected either way — it already sets its
       own explicit height:100% below. */
  }

  .feature-panel__content {
    /* Tablet — brief's own "keep the media larger than the text side";
       44/56 here (vs. the large-desktop tier's exact 5/12–7/12 below)
       because the site's own responsive outer margin (see
       .feature-panel-outer) leaves less room at tablet widths for the
       full desktop inset to still read as "3-4 lines" — confirmed by
       measurement at 834px and 1024px. */
    flex: 0 0 44%;
    width: 44%;
    padding: 0;
  }

  .feature-panel__copy {
    /* Tablet — brief's own "reduce internal text inset". */
    padding: 0 var(--cb-space-4) 0 var(--cb-space-5);
  }

  .feature-panel__title {
    font-size: clamp(2.25rem, 2.1rem + 0.4vw, 2.375rem);
  }

  .feature-panel__visual {
    flex: 0 0 56%;
    width: 56%;
    aspect-ratio: auto;
    height: 100%;
  }
}

@media (min-width: 80rem) {
  /* Large desktop tier proper — the brief's own measured Apple structure,
     exactly: 480px fixed height, 5/12 (41.6667%) copy column, 7/12
     (58.3333%) media column, 72-80px/52-70px copy-wrapper inset. Held off
     until 80rem (1280px), not 50.0625rem: at narrower tablet widths this
     exact split+inset combination leaves too little room for the
     description to hold to "no more than 3-4 lines" (confirmed by
     measurement in an earlier pass), since even this new, capped outer
     wrapper is narrower than 1260px below ~1440px wide. */
  .feature-panel {
    height: 480px;
  }

  .feature-panel__content {
    flex-basis: 41.6667%;
    width: 41.6667%;
  }

  .feature-panel__copy {
    /* 76px/60px — both inside the brief's own "72-80px left / 52-70px
       right" ranges. */
    padding: 0 60px 0 76px;
  }

  .feature-panel__visual {
    flex-basis: 58.3333%;
    width: 58.3333%;
  }
}

/* Respect prefers-reduced-motion: the reveal wrapper simply renders at its
   final scale(1)/translateY(0) with no transition — js/main.js's
   initFeaturePanelReveal also skips arming the animation at all under this
   preference, so .is-armed is never added in the first place; this rule is
   the CSS-side belt-and-braces match for that JS check. */
@media (prefers-reduced-motion: reduce) {
  .feature-panel__reveal {
    transition: none;
  }
}

.feature-panel__reveal.is-armed {
  /* Brief's own "begins at approximately scale(1.025); optionally translate
     by no more than 12px". */
  transform: scale(1.025) translateY(12px);
}

.feature-panel__reveal.is-armed.is-revealed {
  transform: scale(1) translateY(0);
}

/* Les Clés de Charlotte project drawer — a right-aligned in-page panel, not
   a full-screen modal (brief: "this is not... a full-screen modal"). Fixed,
   full-viewport wrapper holding a backdrop and the panel as siblings;
   [inert] (js/main.js's initProjectDrawer, toggled — never a JS-driven
   class) is the single source of truth for both the collapsed visual state
   below and removing the whole drawer from keyboard/screen-reader reach
   while closed, the same pattern the FAQ accordion's own panels already
   use. z-index sits above everything else on the page (the highest
   anywhere in this file) since this is the one true overlay — EXCEPT
   .nav-burger, which is deliberately elevated one step further (201, see
   its own comment) so it stays reachable as the nav drawer's own close
   control. That elevation was found (F-8, final visual review) to also
   put the real burger on top of THIS drawer's own close button, when the
   project drawer opened at a scroll position where the header was still
   on screen — confirmed by a real bounding-box overlap at 390px width.
   Fixed below by hiding the burger for the one drawer that doesn't use it
   as its own close control, rather than raising this z-index further
   (which would just invert the same conflict for the nav drawer). */
.project-drawer {
  position: fixed;
  inset: 0;
  z-index: 200;
  visibility: hidden;
  pointer-events: none;
}

.project-drawer:not([inert]) {
  visibility: visible;
  pointer-events: auto;
}

/* One unambiguous control in the upper-left, not two stacked ones — the
   project drawer has its own dedicated close button (.project-drawer__
   close); the site's real burger toggle is for the nav drawer only and
   has no role here. visibility (not display) keeps the header's layout
   stable — nothing reflows when the drawer opens/closes — matching how
   the drawers themselves already toggle visibility above. Re-evaluates
   automatically whenever [inert] is toggled by the existing drawer open/
   close JS; no new JS needed. */
body:has(.project-drawer:not([inert])) .nav-burger {
  visibility: hidden;
}

.project-drawer__backdrop {
  position: absolute;
  inset: 0;
  /* 35%, inside the brief's own "approximately 28-42%" — restrained enough
     that the left, uncovered portion of the homepage stays clearly legible
     ("prevent the backdrop from making the page entirely black"). */
  background: color-mix(in srgb, var(--cb-color-ink) 35%, transparent);
  opacity: 0;
  transition: opacity 650ms var(--cb-ease-standard);
}

.project-drawer:not([inert]) .project-drawer__backdrop {
  opacity: 1;
}

/* The panel's own opaque Paper background is what visually hides the
   backdrop under the covered (right) portion of the screen — one full-size
   backdrop element is enough, nothing needs to precisely mask just the
   exposed left sliver. Mobile-first: full-viewport width here; the
   ≥50.0625rem/≥64rem tiers below narrow it to the brief's own tablet/
   desktop targets. */
.project-drawer__panel {
  position: absolute;
  top: 0;
  right: 0;
  width: 100vw;
  height: 100svh;
  background: var(--cb-color-paper);
  display: flex;
  flex-direction: column;
  transform: translateX(100%);
  transition: transform 650ms var(--cb-ease-standard);
}

.project-drawer:not([inert]) .project-drawer__panel {
  transform: translateX(0);
}

@media (prefers-reduced-motion: reduce) {
  .project-drawer__backdrop,
  .project-drawer__panel {
    transition: none;
  }
}

@media (min-width: 50.0625rem) {
  .project-drawer__panel {
    /* Tablet: brief's own "approximately 86-92vw". */
    width: 89vw;
  }
}

@media (min-width: 64rem) {
  .project-drawer__panel {
    /* Desktop: brief's own explicit width/max/min-width targets. A real
       floor, not just a formula — 72vw only reaches 880px once the
       viewport itself is ~1222px wide, so below that this deliberately
       holds the drawer at its own readable minimum rather than letting it
       narrow further. */
    width: 72vw;
    max-width: 1320px;
    min-width: 880px;
  }
}

.project-drawer__topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--cb-space-4);
  flex-shrink: 0;
  padding: var(--cb-space-4) var(--cb-space-5);
  border-bottom: var(--cb-border-width) solid var(--cb-color-line);
}

.project-drawer__close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.75rem;
  height: 2.75rem;
  margin: -0.5rem;
  border-radius: var(--cb-radius);
  border: none;
  background: none;
  font-size: 1.5rem;
  line-height: 1;
  color: var(--cb-color-ink);
  cursor: pointer;
}

.project-drawer__wordmark {
  /* aria-hidden + tabindex="-1" in the HTML — a second, purely decorative
     mark inside the drawer, not a second real navigation link back to "/".
     Kept out of both the tab order and the accessibility tree, so this is
     never announced or focusable as a duplicate of the header's own
     wordmark link. */
  pointer-events: none;
}

.project-drawer__wordmark img {
  /* 10.73rem (~172px) — renders at ~18px tall. The first pass here
     preserved the old stacked mark's own rendered height, which read as
     too large for this small, centred, purely decorative drawer mark. */
  width: 10.73rem;
  height: auto;
}

.project-drawer__context-label {
  margin: 0;
  font-family: var(--cb-font-sans);
  font-weight: var(--cb-weight-medium);
  font-size: var(--cb-text-label-size);
  line-height: var(--cb-text-label-leading);
  letter-spacing: var(--cb-text-label-tracking);
  text-transform: uppercase;
  color: var(--cb-color-text-secondary);
}

/* The ONLY element inside the drawer that scrolls — brief: "keep the top
   bar sticky within the drawer while its content scrolls... allow only the
   drawer content to scroll." .project-drawer__topbar's own flex-shrink:0
   above is what keeps it a fixed-height sibling rather than scrolling away
   with this. */
.project-drawer__content {
  flex: 1 1 auto;
  overflow-y: auto;
  padding: var(--cb-space-6) var(--cb-space-5) var(--cb-space-7);
}

.project-drawer__hero {
  max-width: 51.25rem;
  margin-inline: auto;
  text-align: center;
}

.project-drawer__title {
  margin: var(--cb-space-3) 0 var(--cb-space-4);
  font-family: var(--cb-font-sans);
  font-weight: var(--cb-weight-semibold);
  /* Mobile — brief gives no separate mobile number for this one ("54-72px
     on large desktop... responsive"), so this scales down from the
     desktop tier below rather than jumping straight to it. */
  font-size: clamp(2.25rem, 1.8rem + 3vw, 3.375rem);
  line-height: 1.1;
  color: var(--cb-color-ink);
}

.project-drawer__intro {
  margin: 0 auto;
  max-width: 46rem;
  font-family: var(--cb-font-sans);
  font-weight: var(--cb-weight-regular);
  font-size: var(--cb-text-body-size);
  line-height: 1.6;
  color: var(--cb-color-text-secondary);
}

/* The Les Clés de Charlotte hero intro now runs several paragraphs — each
   one is a separate .project-drawer__intro so a screen reader/browser
   "find in page" still sees real paragraph breaks, not one block with
   manually-inserted line breaks. Only the spacing BETWEEN them needed a
   new rule; a lone .project-drawer__intro elsewhere is unaffected. */
.project-drawer__intro + .project-drawer__intro {
  margin-top: var(--cb-space-4);
}

/* Shared by the one large hero visual and each section's own supporting
   slot — brief: "a clearly marked project-visual slot... subtle Line-token
   border... never exceed approximately 70-78svh". Same plain placeholder
   treatment as the landing panel's own visual, not the dot pattern. */
.project-drawer__visual {
  margin-top: var(--cb-space-6);
  display: flex;
  align-items: center;
  justify-content: center;
  aspect-ratio: 16 / 9;
  max-height: 75svh;
  border-radius: var(--cb-radius);
  border: 1px solid var(--cb-color-line);
  background: color-mix(in srgb, var(--cb-color-ink) 2%, var(--cb-color-paper));
  padding: var(--cb-space-4);
  text-align: center;
}

.project-drawer__visual-label {
  margin: 0;
  max-width: 26ch;
  font-family: var(--cb-font-sans);
  font-weight: var(--cb-weight-medium);
  font-size: var(--cb-text-label-size);
  line-height: var(--cb-text-label-leading);
  letter-spacing: var(--cb-text-label-tracking);
  text-transform: uppercase;
  color: var(--cb-color-text-secondary);
}

.project-drawer__section {
  max-width: 51.25rem;
  margin: var(--cb-space-9) auto 0;
}

.project-drawer__eyebrow {
  margin: 0;
  font-family: var(--cb-font-sans);
  font-weight: var(--cb-weight-medium);
  font-size: var(--cb-text-label-size);
  line-height: var(--cb-text-label-leading);
  letter-spacing: var(--cb-text-label-tracking);
  text-transform: uppercase;
  color: var(--cb-color-text-secondary);
}

.project-drawer__heading {
  margin: var(--cb-space-3) 0 var(--cb-space-4);
  font-family: var(--cb-font-sans);
  font-weight: var(--cb-weight-semibold);
  font-size: clamp(1.75rem, 1.5rem + 1.2vw, 2.375rem);
  line-height: 1.25;
  color: var(--cb-color-ink);
}

.project-drawer__text {
  margin: 0;
  max-width: 48rem;
  font-family: var(--cb-font-sans);
  font-weight: var(--cb-weight-regular);
  font-size: clamp(1.0625rem, 1rem + 0.25vw, 1.25rem);
  line-height: 1.6;
  color: var(--cb-color-text-secondary);
}

.project-drawer__footer {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--cb-space-4);
  max-width: 51.25rem;
  margin: var(--cb-space-9) auto 0;
  padding-top: var(--cb-space-6);
  border-top: var(--cb-border-width) solid var(--cb-color-line);
  text-align: center;
}

.project-drawer__footer-logo {
  /* 13.11rem (~210px) — renders at ~22px tall. The first pass here
     preserved the old stacked mark's own rendered height, which read as
     too large for this closing mark. */
  width: 13.11rem;
  height: auto;
}

.project-drawer__footer-close {
  font-family: var(--cb-font-sans);
  font-weight: var(--cb-weight-medium);
  font-size: var(--cb-text-small-size);
  color: var(--cb-color-text-secondary);
  background: none;
  border: none;
  text-decoration: underline;
  cursor: pointer;
}

/* ==========================================================================
   LES CLÉS DE CHARLOTTE CASE STUDY — media placeholders + editorial
   compositions, scoped to this one project drawer only (lcdc- prefix).
   Everything below reuses the drawer's own existing tokens/typography
   (.project-drawer__eyebrow/__heading/__text stay exactly as they were and
   are reused unchanged for every new section) rather than inventing a
   parallel type scale. Nothing here is a new drawer/modal component — it
   only fills the existing #project-drawer with richer content.
   ========================================================================== */

/* AT A GLANCE — plain numbers, no cards/icons/colour, per brief. Reuses
   .project-drawer__section for its own outer spacing; only the grid of
   stats themselves is new. 2x2 by default, one row of four from the
   tablet breakpoint already used everywhere else on this page. */
.lcdc-stats {
  margin: var(--cb-space-6) 0 0;
  padding: 0;
  list-style: none;
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--cb-space-6) var(--cb-space-4);
}

@media (min-width: 50.0625rem) {
  .lcdc-stats {
    grid-template-columns: repeat(4, 1fr);
  }
}

.lcdc-stat__number {
  margin: 0;
  font-family: var(--cb-font-sans);
  font-weight: var(--cb-weight-medium);
  font-size: clamp(2.25rem, 1.9rem + 1.6vw, 3rem);
  line-height: 1.05;
  letter-spacing: -0.02em;
  color: var(--cb-color-ink);
}

.lcdc-stat__label {
  margin: var(--cb-space-2) 0 0;
  font-family: var(--cb-font-sans);
  font-weight: var(--cb-weight-medium);
  font-size: var(--cb-text-label-size);
  line-height: var(--cb-text-label-leading);
  letter-spacing: var(--cb-text-label-tracking);
  text-transform: uppercase;
  color: var(--cb-color-text-secondary);
}

/* MEDIA PLACEHOLDER — one reusable component for every lcdc-media-* slot.
   Same visual language as the original .project-drawer__visual (subtle
   Line border, faint Ink-on-Paper tint, --cb-radius, centred label) so a
   later swap-in doesn't need a second visual language. .lcdc-media__frame
   is deliberately the ONLY element that carries the aspect-ratio/border/
   background — replacing its text content with a real <img>/<video>
   (object-fit: cover) later requires no layout change at all.

   Simplification pass: this case study previously carried 8 placeholder
   slots (11 individual assets) with multi-sentence production-instruction
   copy inside each one — .lcdc-media__desc/__hint/__caption, and the
   triptych/mobile-duo/testimonial/closing components that hung off them.
   Reduced to 4 slots (.lcdc-media__index label only, no instructional
   copy in the public-facing layout) so the case study reads as premium
   and concise rather than a scrolled-through production brief; see git
   history for the previous, fuller version if any of that is needed
   again. */
.lcdc-media {
  margin: var(--cb-space-6) 0 0;
}

.lcdc-media__frame {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--cb-space-2);
  width: 100%;
  border-radius: var(--cb-radius);
  border: 1px solid var(--cb-color-line);
  background: color-mix(in srgb, var(--cb-color-ink) 2%, var(--cb-color-paper));
  padding: var(--cb-space-4);
  text-align: center;
}

.lcdc-media__index {
  margin: 0;
  font-family: var(--cb-font-sans);
  font-weight: var(--cb-weight-medium);
  font-size: var(--cb-text-label-size);
  line-height: var(--cb-text-label-leading);
  letter-spacing: var(--cb-text-label-tracking);
  text-transform: uppercase;
  color: var(--cb-color-ink);
}

/* Aspect-ratio modifiers — one per intended final asset shape. No
   overflow:hidden here deliberately: during this placeholder phase the
   box is allowed to grow slightly taller than the nominal ratio rather
   than ever clipping the label; once a real <img>/<video> (with
   object-fit: cover) replaces the placeholder text, add overflow:hidden
   back alongside it to crop cleanly to the ratio. */
.lcdc-media--16-9 .lcdc-media__frame {
  aspect-ratio: 16 / 9;
}

.lcdc-media--4-3 .lcdc-media__frame {
  aspect-ratio: 4 / 3;
}

.lcdc-media--3-4 .lcdc-media__frame {
  aspect-ratio: 3 / 4;
}

/* Hero film — the one large placeholder immediately under the intro copy,
   given a touch more top space than the in-section media below it. */
.lcdc-media--hero {
  margin-top: var(--cb-space-7);
}

/* TWO-UP COMPOSITION — physical world -> digital translation (Challenge &
   solution). Equal-width columns with each item's own aspect ratio (not a
   shared one) is what produces the uneven, editorial rhythm the brief
   asks for — a portrait photo and a wider screenshot naturally settle at
   different heights side by side without any manual overlap/positioning. */
.lcdc-duo {
  margin-top: var(--cb-space-6);
  display: grid;
  grid-template-columns: 1fr;
  align-items: start;
  gap: var(--cb-space-5);
}

.lcdc-duo .lcdc-media {
  margin-top: 0;
}

@media (min-width: 50.0625rem) {
  .lcdc-duo {
    grid-template-columns: 1fr 1fr;
  }

  /* A small, deliberate stagger — brief: "an editorial two-column /
     overlapping visual composition" for the Challenge & solution duo. */
  .lcdc-duo--stagger > :nth-child(2) {
    margin-top: var(--cb-space-8);
  }
}

/* REAL MEDIA (final review — LCDC media pass): the placeholder frame above
   is text-centred with padding, deliberately not cropped; a real video or
   image needs the opposite — full-bleed within the frame, cropped cleanly
   to its aspect ratio. Modifier classes, not a rework of .lcdc-media__frame
   itself, so nothing about the (still-reusable) placeholder pattern above
   changes. */
.lcdc-media__frame--video,
.lcdc-media__frame--image {
  padding: 0;
  overflow: hidden;
  /* A quiet neutral fill visible for the brief instant before the poster/
     first frame paints — never a jarring flash of bare Paper. */
  background: var(--cb-color-line);
}

.lcdc-media__video,
.lcdc-media__image {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Portrait modifier — the wedding/growth-opportunity video keeps its own
   portrait format rather than being stretched into a landscape box (brief,
   explicit). */
.lcdc-media--9-16 .lcdc-media__frame {
  aspect-ratio: 9 / 16;
}

/* CORRECTION PASS: the villa-collection screenshot's own real proportions
   (1930x991, ≈1.95:1) don't match the generic 16:9 (≈1.78:1) box above —
   object-fit:cover was cropping its sides to force-fit the mismatched
   ratio, which is exactly the "cut/cropped, doesn't show the full page"
   defect reported. Fix: this modifier drops the forced aspect-ratio
   entirely, so the frame's height is simply whatever the image's own
   intrinsic ratio produces at 100% width — the full screenshot, always,
   with nothing to crop. object-fit:contain stays as a safety net (a
   no-op here since the frame never over/under-shoots the image's own
   ratio), matching the brief's own explicit "object-fit: contain logic"
   request. */
.lcdc-media--natural .lcdc-media__frame {
  aspect-ratio: auto;
}

.lcdc-media--natural .lcdc-media__image {
  height: auto;
  object-fit: contain;
}

/* TIGHT CORRECTION PASS: the villa screenshot is no longer cropped (see
   .lcdc-media--natural above), but sitting inside a whole-section
   .lcdc-card left it feeling like a small image lost in a big empty box
   — real feedback, not a style guess. The section-level card is gone
   (Brand is a normal editorial section again); this modifier instead
   gives the media FRAME ITSELF the more deliberate, slightly larger
   radius (matching --cb-radius-card, the sitewide "standard compact
   card" token) so the screenshot reads as one elegant, tightly-framed
   object — "an elegant framed media treatment", not an oversized
   container. */
.lcdc-media--framed .lcdc-media__frame {
  border-radius: var(--cb-radius-card);
}

/* GUEST / OWNER CARDS ONLY (correction pass — this is the actual scope
   of the brief's card request; the previous pass wrapped whole SECTIONS
   in cards, which was a misread). Light-grey tinted surface (not the
   plain-Paper .lcdc-media__frame tint, and not a bordered white card
   either — brief: "light grey background... rounded corners... clean
   internal spacing... elegant, understated"), applied directly to each
   .lcdc-audience__item. The section itself (heading + intro) stays
   normal page content, outside any card. */
.lcdc-audience__item.lcdc-card {
  padding: var(--cb-space-5);
  border-radius: var(--cb-radius-card);
  background: color-mix(in srgb, var(--cb-color-ink) 4%, var(--cb-color-paper));
}

.lcdc-audience__item.lcdc-card > .project-drawer__eyebrow:first-child {
  margin-top: 0;
}

@media (min-width: 50.0625rem) {
  .lcdc-audience__item.lcdc-card {
    padding: var(--cb-space-6);
  }
}

/* CUSTOMER EXPERIENCE — guest/owner static two-column storytelling. Not
   another .lcdc-duo (that component is specifically for two independent
   MEDIA items side by side) — kept as its own small component since the
   guest/owner items are primarily eyebrow+heading+text, each now its own
   card (see .lcdc-audience__item.lcdc-card above). */
.lcdc-audience {
  margin-top: var(--cb-space-6);
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--cb-space-5);
}

@media (min-width: 50.0625rem) {
  .lcdc-audience {
    grid-template-columns: 1fr 1fr;
    gap: var(--cb-space-5);
  }
}

.lcdc-audience__heading {
  margin: var(--cb-space-2) 0 var(--cb-space-3);
  font-family: var(--cb-font-sans);
  font-weight: var(--cb-weight-semibold);
  font-size: clamp(1.25rem, 1.1rem + 0.6vw, 1.5rem);
  line-height: 1.25;
  color: var(--cb-color-ink);
}

/* BUILT FOR GROWTH: text beside the video on desktop. The eyebrow/heading
   live inside .lcdc-growth-text (not above the grid) specifically so the
   media column's height — via grid stretch below — spans the same top-
   to-bottom range as the whole text block, title included: that's what
   lets the media top-align with the heading and bottom-align with the
   last paragraph without any manual offset math. Mobile/tablet stack
   naturally (video below text) — "beside it" is desktop-specific. */
.lcdc-growth {
  margin-top: var(--cb-space-6);
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--cb-space-6);
}

.lcdc-growth-text > .project-drawer__text + .project-drawer__text {
  margin-top: var(--cb-space-4);
}

/* FINAL REFINEMENT: no grey card/background behind the media — it now
   sits directly in the layout as media, not inside a decorative surface.
   4:5 (not the raw 9:16 capture ratio, which reads as a phone/story-video
   shape rather than an editorial one) via object-fit:cover with a
   calibrated object-position (the real subjects in this footage sit in
   the upper-middle of frame, not dead centre — checked against the
   poster frame directly). This crops, it never stretches — the aspect
   ratio stays locked at every size. */
.lcdc-growth-video {
  position: relative;
  margin-top: 0;
  width: 100%;
  max-width: 22rem;
  margin-inline: auto;
}

.lcdc-growth-video .lcdc-media__frame {
  aspect-ratio: 4 / 5;
}

.lcdc-growth-video .lcdc-media__video {
  object-fit: cover;
  object-position: 50% 30%;
}

@media (min-width: 50.0625rem) {
  /* A fixed-width video column (not "auto" sized from an aspect-ratio +
     stretched height) deliberately avoids a real sizing cycle: letting the
     video's width be DERIVED from a stretched height fed the video's own
     growing width back into how much room was left for the text, which
     made the text wrap onto far more lines, growing the row even taller,
     in a runaway loop (measured: the video ballooned past 2000px tall).
     With the video's width fixed, the text column's width — and so its
     wrapped height — is fully independent of the video, and align-items:
     stretch grows the video down to match that stable height. The video
     frame is also switched to position:absolute inside its (now
     position:relative) grid-item wrapper: without that, the <video>'s own
     native pixel dimensions (it's a portrait 1080x1920 file) leak into the
     grid's row-height calculation as the frame's intrinsic size, stretching
     the row to the video's OWN natural aspect instead of the text's actual
     height. Taking the frame out of flow removes that intrinsic
     contribution entirely, so the row height comes from the text column
     alone. object-fit:cover on the <video> itself means the source footage
     is only ever cropped to fill the resulting box, never stretched — the
     asset's own pixel proportions are untouched. */
  .lcdc-growth {
    grid-template-columns: minmax(0, 1fr) 24rem;
    align-items: stretch;
    gap: var(--cb-space-7);
  }

  .lcdc-growth-video {
    width: 100%;
    max-width: none;
    margin-inline: 0;
  }

  .lcdc-growth-video .lcdc-media__frame {
    position: absolute;
    inset: 0;
    height: 100%;
    width: 100%;
    aspect-ratio: auto;
  }
}

/* Scroll lock while the drawer is open (js/main.js's initProjectDrawer AND
   initBookingDrawer both toggle this same class — only one drawer can ever
   be open at once, since opening either marks `main` inert, which is where
   both triggers live) — overflow:hidden alone preserves the homepage's own
   scrollTop exactly (brief: "preserve the homepage's exact scroll
   position"), so no position:fixed/scroll-restore trick is needed; JS
   separately sets an inline padding-right compensating for the now-hidden
   scrollbar's own width, to satisfy "prevent layout shift when body
   scrolling is locked". */
body.has-open-drawer {
  overflow: hidden;
}

/* Direct-booking drawer — same reliable right-side-panel mechanics as
   .project-drawer above (fixed full-viewport wrapper, [inert] as the single
   source of truth for the collapsed state, backdrop + panel as siblings,
   translateX slide-in), but its own width (brief: "do not share its width
   or project-specific content" — this needs real room for a future
   embedded scheduler, .project-drawer's own width is unrelated) and its own
   two internal states (details form / calendar), never the project
   drawer's long-form scrolling content. Deliberately a fully separate rule
   set and JS controller (initBookingDrawer, not a shared function) rather
   than parameterising .project-drawer's own — see that block's comments
   for why touching it at all is out of scope here. */
.booking-drawer {
  position: fixed;
  inset: 0;
  z-index: 200;
  visibility: hidden;
  pointer-events: none;
}

.booking-drawer:not([inert]) {
  visibility: visible;
  pointer-events: auto;
}

.booking-drawer__backdrop {
  position: absolute;
  inset: 0;
  background: color-mix(in srgb, var(--cb-color-ink) 35%, transparent);
  opacity: 0;
  transition: opacity 650ms var(--cb-ease-standard);
}

.booking-drawer:not([inert]) .booking-drawer__backdrop {
  opacity: 1;
}

/* Mobile-first: 100vw here (brief's own explicit mobile target); the
   ≥50.0625rem/≥64rem tiers below narrow it to the brief's tablet/desktop
   targets. Desktop deliberately stays much narrower than .project-drawer's
   own 72vw/1320px — brief: "approximately 40vw... do not restrict the
   desktop drawer to 33vw" (668px-ish min room for a future calendar embed,
   not a long-form reading measure). */
.booking-drawer__panel {
  position: absolute;
  top: 0;
  right: 0;
  width: 100vw;
  height: 100svh;
  background: var(--cb-color-paper);
  display: flex;
  flex-direction: column;
  transform: translateX(100%);
  transition: transform 650ms var(--cb-ease-standard);
}

.booking-drawer:not([inert]) .booking-drawer__panel {
  transform: translateX(0);
}

@media (prefers-reduced-motion: reduce) {
  .booking-drawer__backdrop,
  .booking-drawer__panel {
    transition: none;
  }
}

@media (min-width: 50.0625rem) {
  .booking-drawer__panel {
    /* Tablet — brief's own "approximately 72-82vw". */
    width: 78vw;
  }
}

@media (min-width: 64rem) {
  .booking-drawer__panel {
    /* Large desktop — brief's own explicit width/min/max targets. */
    width: 40vw;
    min-width: 520px;
    max-width: 680px;
  }
}

.booking-drawer__topbar {
  display: flex;
  justify-content: flex-end;
  flex-shrink: 0;
  padding: var(--cb-space-4) var(--cb-space-4) 0;
}

@media (min-width: 50.0625rem) {
  .booking-drawer__topbar {
    padding: var(--cb-space-4) var(--cb-space-6) 0;
  }
}

@media (min-width: 64rem) {
  .booking-drawer__topbar {
    padding: var(--cb-space-4) var(--cb-space-7) 0;
  }
}

/* Same visual treatment as .project-drawer__close (plain hit-area, no
   filled decorative circle, transparent-margin bigger tap target) —
   reproduced rather than reused directly since that class belongs to a
   component this task must not touch or couple to. */
.booking-drawer__close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.75rem;
  height: 2.75rem;
  margin: -0.5rem;
  border-radius: var(--cb-radius);
  border: none;
  background: none;
  font-size: 1.5rem;
  line-height: 1;
  color: var(--cb-color-ink);
  cursor: pointer;
}

/* The only element inside the drawer that scrolls — matches
   .project-drawer__content's own "topbar stays put, content scrolls"
   split; .booking-drawer__topbar's own flex-shrink:0 above is what keeps
   it a fixed-height sibling rather than scrolling away with this.
   Suggested padding tiers from the brief: ~24-28px mobile, ~40-48px
   tablet, ~48-64px desktop. */
.booking-drawer__content {
  flex: 1 1 auto;
  overflow-y: auto;
  padding: 0 var(--cb-space-4) var(--cb-space-6);
}

@media (min-width: 50.0625rem) {
  .booking-drawer__content {
    padding: 0 var(--cb-space-6) var(--cb-space-7);
  }
}

@media (min-width: 64rem) {
  .booking-drawer__content {
    padding: 0 var(--cb-space-7) var(--cb-space-7);
  }
}

.booking-drawer__heading {
  margin: var(--cb-space-3) 0 0;
  font-family: var(--cb-font-sans);
  font-weight: var(--cb-weight-semibold);
  font-size: var(--cb-text-page-heading-size);
  line-height: var(--cb-text-page-heading-leading);
  letter-spacing: var(--cb-text-page-heading-tracking);
  outline: none;
}

.booking-drawer__supporting {
  margin-top: var(--cb-space-4);
  color: var(--cb-color-text-secondary);
}

/* No spacing rules of its own between fields needed beyond this top offset
   — .form-row's own sitewide :not(:first-child) rule and .form-field--full
   (both already loaded via layout.css/components.css) already produce the
   right rhythm between the Name field, the Email/Company row and the
   Discussion field without repeating either rule here. */
.booking-drawer__form {
  margin-top: var(--cb-space-6);
  display: flex;
  flex-direction: column;
}

/* .cta.booking-drawer__submit (two classes), not .booking-drawer__submit
   alone — components.css's own unconditional .cta rule sets the full
   `margin` shorthand (including margin-top: -0.5em, its own bigger-tap-
   target trick), which at equal specificity silently overrode this same
   rule's plain margin-top before (measured: -8.5px computed, not the
   intended spacing — exactly why the CTA sat too close to the field
   above). The extra class guarantees this wins regardless of source order.
   Values are the brief's own responsive targets: ~24-28px mobile, ~28-32px
   tablet, ~28-36px desktop. */
.cta.booking-drawer__submit {
  margin-top: 26px;
  align-self: flex-start;
}

@media (min-width: 50.0625rem) {
  .cta.booking-drawer__submit {
    margin-top: 30px;
  }
}

@media (min-width: 64rem) {
  .cta.booking-drawer__submit {
    margin-top: var(--cb-space-5);
  }
}

/* [hidden] needs no extra rule here — .booking-drawer__state sets no
   display property of its own, so the UA stylesheet's default
   [hidden]{display:none} already applies without the author-rule-always-
   wins gotcha .contact-flow__success elsewhere had to work around. */
/* .cta.booking-drawer__back (two classes), not .booking-drawer__back alone
   — components.css's own unconditional .cta rule (display:inline-flex)
   loads AFTER this file, so an equal-specificity override here would
   silently lose the cascade regardless of which one reads more specific to
   this component; the extra class is what guarantees this wins. Same
   gotcha layout.css's own .site-footer .cta rule already documents.
   display:flex (not inline-flex) is what actually puts this on its own
   line above the eyebrow instead of running into it inline — the internal
   icon/label alignment .cta relies on is unaffected, since flex and
   inline-flex only differ in outer display. */
.cta.booking-drawer__back {
  display: flex;
  margin-bottom: var(--cb-space-6);
}

/* Real Cal.com inline embed container — js/cal-booking.js's
   mountBookingCalendar mounts into this exact element (matched by
   [data-calendar-embed]). 24px radius matches the equivalent container
   built for the guided contact page's own Cal.com embed
   (css/contact-flow.css) — the two are the same eventual component on
   different pages, kept visually identical on purpose. Uses the full
   available drawer width — no artificial max-width here, unlike the
   guided-enquiry page's own much wider container (see that rule's own
   comment for why that one needs centring and a cap).

   height:auto (no fixed/min-height) once ready: Cal.com's own official
   embed resizes its iframe responsively as the visitor moves between the
   calendar, the review-and-confirm form and the confirmation view, so this
   wrapper simply follows that real height rather than reserving one fixed
   block of space for every internal Cal.com screen. overflow:visible so
   that resize is never clipped; the padding below is what keeps the
   iframe's own square corners clear of this box's rounded ones without
   needing overflow:hidden to fake it. */
.booking-drawer__calendar-embed {
  position: relative;
  margin-top: var(--cb-space-6);
  width: 100%;
  height: auto;
  border-radius: var(--cb-radius-modal);
  background: color-mix(in srgb, var(--cb-color-ink) 8%, var(--cb-color-paper));
  padding: 20px;
  overflow: visible;
}

/* Temporary floor while waiting for Cal's "linkReady"/"bookerReady"
   (js/cal-booking.js toggles this class) — removed the moment either
   fires, so a ready embed's real height is never held back by it. */
.booking-drawer__calendar-embed.is-loading {
  min-height: 520px;
}

@media (min-width: 50.0625rem) {
  .booking-drawer__calendar-embed.is-loading {
    min-height: 430px;
  }
}

@media (min-width: 64rem) {
  .booking-drawer__calendar-embed.is-loading {
    min-height: 460px;
  }
}

/* Process — a sticky, scroll-driven storytelling section, not a normal
   static block. .process itself is a TALL scroll TRACK, not the visible
   content box: 100vh of pinned viewport + 4 stages x 80vh of scroll-through
   each = 420vh total. .process__sticky is what actually pins
   (position:sticky; top:0) while the user scrolls through that height, so
   the next section physically cannot appear until the full 420vh has been
   scrolled — the four stages are seen in order by construction, with no
   scroll-hijacking/preventDefault anywhere (js/main.js's initProcess only
   ever reads scroll position via getBoundingClientRect(), never
   intercepts it). This 420vh figure and initProcess's own scrollable-
   distance math (section height minus one viewport) must move together if
   either ever changes.

   Two columns inside the pinned viewport — left: fixed eyebrow/CTA plus a
   stage title/paragraph that crossfades between four stacked variants;
   right: one large multi-ring dotted circle with its own crossfading inner
   heading/paragraph. Stacks to one column below the same 50.0625rem
   breakpoint every other two-column section on this page uses; the ring
   sits below the text there (order in the DOM), same as every other
   two-column section's mobile stack. */
.process {
  position: relative;
  height: 420vh;
  /* This section's share (top and bottom) of the shared
     2×var(--cb-section-gap) section-to-section gap — margin, not padding:
     padding would inflate this element's own height, which js/main.js's
     initProcess reads directly (scrollable = section.offsetHeight −
     window.innerHeight) to drive the scroll-jacked progress math. Margin
     isn't part of offsetHeight and doesn't shift getBoundingClientRect()
     .top's own zero-point logic, so it's safe to add here without touching
     that calculation. */
  margin-top: var(--cb-section-gap);
  margin-bottom: var(--cb-section-gap);
}

.process__sticky {
  position: sticky;
  top: 0;
  height: 100vh;
  height: 100dvh; /* better on mobile, where the browser chrome changes the "real" viewport height; the plain 100vh above is a safe fallback for browsers without dvh support, not a bug */
  display: flex;
  align-items: center;
  overflow: hidden;
}

.process__container {
  /* No max-width/margin-inline centring cap here, unlike .faq__container/
     .contact__container below — this section's own brief is explicit that
     the graphic must anchor to the true viewport edge at wide desktop
     ("confident right-edge placement... do not centre the graphic within
     the page... do not create equal left and right margins around the
     complete two-column composition"), which a centred, capped island
     would directly contradict past ~1200px. Matches .capabilities__grid
     (also two-column, also uncapped) rather than the single-column
     FAQ/contact containers — the plain .container class this element also
     carries already gives the fixed 100px/90px edge inset every section
     uses; only the width cap is removed. */
  width: 100%;
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--cb-space-5);
  /* Deliberately tight below the two-column breakpoint — this whole
     section is pinned inside a fixed 100dvh viewport (.process__sticky)
     while stacked, so every px spent on outer padding here is a px taken
     directly from the room available to the text+circle stack beneath it.
     A first pass reusing the same space-6 padding/space-7 gap this section
     used at the two-column tiers measured the stacked mobile content at
     945px tall against an 844px-tall iPhone viewport — the excess got
     silently clipped equally off the top (the eyebrow) and bottom (the
     circle) by .process__sticky's overflow:hidden, found by screenshot,
     not assumed. Restored to the more generous space-6/space-7 values
     below 50.0625rem, where the two-column layout has no such vertical
     ceiling to fit inside. */
  padding-block: var(--cb-space-3);
}

/* Raised from 50.0625rem to 64.0625rem (same tablet/desktop realignment as
   .hero__dot-field/.capabilities__grid above) — the two-column composition
   crammed into a genuine tablet width (e.g. 820px) compressed the ring's
   own inner text badly and left the left column mostly empty (confirmed by
   screenshot), reading as "desktop forced into tablet" rather than a
   composition designed for it. Below 64rem now stays single-column/
   stacked, the same flow already used successfully on mobile. */
@media (min-width: 64.0625rem) {
  .process__container {
    grid-template-columns: minmax(0, 38%) minmax(0, 1fr);
    align-items: center;
    gap: clamp(4rem, 9vw, 8.75rem);
    padding-block: var(--cb-space-6);
  }
}

/* Only needed once the two columns sit side by side (below that, .process__intro's
   own CTA already sits directly above this in normal flow with the container's
   gap between them) — closes the gap this section's div order would otherwise
   leave between the intro block and the graphic on narrow/stacked layouts.
   space-4, not this section's usual space-7, below the two-column
   breakpoint — same fixed-100dvh-height reasoning as .process__container's
   own comment above. */
.process__graphic {
  margin-top: var(--cb-space-4);
}

@media (min-width: 64.0625rem) {
  .process__graphic {
    margin-top: 0;
  }
}

@media (min-width: 64rem) {
  /* Desktop layout refinement. Left inset stays --cb-edge-inset-start
     (100px), matching every other section's grid line. Column nudged
     30% -> 28%, still inside the "approximately 28-32%" target an earlier
     round established. Right inset tightened (was clamp(24px,2.7vw,64px))
     so the ring sits closer to the true viewport edge — confirmed by
     measurement to still leave the now-larger ring real slack inside its
     own column at 1440px and 1920px, not just fit exactly.

     Note on `gap`: this grid gap alone does NOT control the actual visual
     distance between the left copy and the ring — .process__ring is
     right-flush (margin-inline:auto 0) to the true viewport edge
     (independent of column width or gap, confirmed by measurement), so
     its real lever for "more breathing room" is .process__intro's own
     max-width below, not this value. Kept generous regardless, purely so
     the two column BOXES themselves never crowd each other. Right inset
     tightened again this pass (was clamp(18px,2vw,48px)) — brief: "much
     closer to the right page edge" — confirmed by measurement to still
     leave the ring real slack in its column (see .process__ring's own
     ≥90rem comment below for the exact numbers). */
  .process__container {
    padding-inline: var(--cb-edge-inset-start) clamp(14px, 1.6vw, 40px);
    grid-template-columns: minmax(0, 28%) minmax(0, 1fr);
    gap: clamp(6rem, 10vw, 14rem);
  }

  /* The actual "more breathing room" lever, per the note above: since the
     ring's own position is fixed independently of this column's width, the
     only way to genuinely grow the visible gap is to narrow the text
     column's own natural width. 300px (was 320px) reads as a deliberate,
     narrow editorial column (short heading + 2-3-line paragraph) rather
     than a wide block that happens to have short lines — and, measured,
     adds real whitespace before the ring at every desktop width from
     1024px up. */
  .process__intro {
    max-width: 300px;
  }
}

/* Laptop-only tightening (1024-1439px): the shared clamp(6rem,10vw,14rem)
   gap above reads fine once it grows toward 1440/1920 (144-192px, already
   balanced — left untouched there), but at 1280px specifically it still
   computed to 128px, which combined with the intro column's own 300px cap
   inside a wider ~358px column left the "Diagnose" text block reading as
   visibly detached from the ring (confirmed by screenshot) rather than one
   composition. Scoped to stop before ≥90rem so the large-desktop tier's
   own already-approved spacing is completely unaffected. */
@media (min-width: 64rem) and (max-width: 89.9375rem) {
  .process__container {
    gap: clamp(3rem, 5vw, 4rem);
  }
}

/* The ring — .process__ring-frame (the square multi-layer dot-circle) and
   .process__ring-content (the variable-length editorial text) are stacked
   in the SAME grid cell rather than the text being absolutely inset into a
   fixed square: the frame stays a perfect circle sized to the ring's own
   width, and the shared cell's height grows to fit the content if the text
   ever needs more vertical room than that circle's diameter provides —
   achieved by CSS grid sizing alone, no JS measurement needed this time
   (the brief's own "smaller inner text, more white space" requirement
   keeps the text comfortably inside the circle at every breakpoint,
   verified by screenshot rather than assumed).

   Full-bleed below the two-column breakpoint — same technique already
   used by .capabilities__visual for the same reason: a decorative circular
   graphic that needs more width than its own (padded, single-column)
   container gives it. Reverted to a normal, contained width once the
   two-column layout starts; from there, margin-inline pushes it toward
   the column's own right edge instead of centring it — brief: "position it
   a little closer to the right page edge" — rather than flush against the
   true viewport edge, which would abandon the shared container's own
   edge-inset rhythm every other section on the page uses. */
.process__ring {
  display: grid;
  width: 100vw;
  margin-inline: calc(50% - 50vw);
}

@media (min-width: 64.0625rem) {
  .process__ring {
    width: 100%;
    margin-inline: auto 0;
    max-width: 34rem;
  }
}

@media (min-width: 64rem) {
  .process__ring {
    /* Intermediate tier (1024-1439px, i.e. below the explicit ≥90rem width
       rule further down): still just a max-width cap on a fluid
       width:100% (inherited from the ≥64.0625rem rule above), so the ring
       keeps scaling smoothly with the column right up to the point the
       ≥90rem tier's fixed width formula takes over. 48.75rem (780px)
       chosen to land exactly on that formula's own floor at 1440px
       (clamp(780px,53vw,970px) = 780px there), so the handoff between the
       two rules doesn't read as a visible jump. */
    max-width: 48.75rem;
  }
}

@media (min-width: 90rem) {
  .process__ring {
    /* Large-desktop sizing: an actual width rule, not transform:scale(),
       so the SVG itself renders at native resolution rather than being
       stretched. Bumped again this pass — brief: "the circle should be
       larger than it is now" — 780px at 1440px (up from 740px) growing to
       a 970px ceiling at 1920px+ (up from 930px). Confirmed by measurement
       to still fit inside the column with real slack at both sizes (not
       just this arithmetic), and to still fit vertically inside the
       100dvh sticky viewport without clipping. max-width:none overrides
       the intermediate 48.75rem cap above, which would otherwise fight
       this explicit width. */
    width: clamp(780px, 53vw, 970px);
    max-width: none;
  }
}

.process__ring-frame,
.process__ring-content {
  grid-area: 1 / 1;
  align-self: center;
  justify-self: center;
}

.process__ring-frame {
  position: relative;
  width: 100%;
  aspect-ratio: 1;
}

.process__ring-dots {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

/* No transition on r/fill this time, deliberately — these are now scroll-
   linked (main.js's initProcess repaints every dot on every scroll-driven
   animation frame), so they need to track the user's own scroll position
   exactly; a transition would make the sweep visibly lag behind fast
   scrolling instead of feeling directly tied to it. The stage TEXT below
   still animates on a timed transition, since that's a discrete, quarter-
   threshold event, not a continuous scroll-position readout. */

/* Smaller inner text with generous, controlled room around it — brief:
   "reduce the text size... create more whitespace between the text block
   and the surrounding dots... use a controlled inner content width so the
   text wraps elegantly". max-width narrowed further this pass (50%, was
   60%) and padding-block increased again (16%, was 12%) — together with
   the smaller font sizes in components.css, this keeps the statement/
   paragraph pair clearly clear of the dots on every side, with real
   margin rather than a near-miss, at every breakpoint. */
.process__ring-content {
  position: relative;
  display: grid;
  max-width: 50%;
  padding-block: 16%;
}

/* Large-desktop content column, per brief: a strict, ABSOLUTE max-width
   (approximately 440-470px) rather than the percentage-of-ring-width rule
   above, so the text block reads as a deliberately composed, fixed-size
   object rather than one that simply grows alongside the now-much-bigger
   ring. padding-block dropped to 0 — at this ring size (780-970px
   diameter) the shared grid cell is already far taller than this content
   needs, so align-self:center (above) alone leaves substantial white
   space above/below without any extra padding forcing the cell taller
   than necessary.

   justify-self switched from centre to a fixed translateX offset — a
   previous pass shifted this LEFT of centre; with the text left-aligned
   inside the box (below), that combination left the actual glyphs hugging
   the circle's own left inner edge, reading as too close to it rather
   than optically centred in the available white space (brief). Flipped to
   a modest RIGHT offset instead — left-aligned text's natural visual
   weight sits toward the left of its own box, so shifting the box itself
   right is what balances that weight around the circle's true centre. A
   transform (not a margin/justify-self change) is what keeps this exact
   +32px offset identical across all four stages regardless of which
   variant's own natural width is current — verified by measurement that
   +32px still leaves real, generous clearance from the innermost row on
   every side, at both 1440px and 1920px. */
@media (min-width: 90rem) {
  .process__ring-content {
    max-width: 470px;
    padding-block: 0;
    transform: translateX(32px);
  }
}

.process__ring-stage {
  grid-area: 1 / 1;
  /* align-self:start, not centre — the shared grid cell (.process__ring-
     content) is already sized to the TALLEST of the four variants and is
     itself vertically centred within the ring (see .process__ring-frame,
     .process__ring-content above), same technique as the left column's
     .process__stage-stack. With align-self:centre, each variant's own
     (different) height instead re-centred INSIDE that fixed cell every
     stage change, so the Step label's own on-screen position drifted by
     up to 28px between stages — a real, measured bug, not a stylistic
     choice. align-self:start anchors every variant to the exact same
     top-left point of that fixed cell, locking the label and paragraph's
     position identically across all four stages. */
  align-self: start;
  justify-self: center;
  /* Left-aligned, not centred — brief: "left-align the Step label and
     paragraph... a more editorial composition" (was text-align:center). */
  text-align: left;
  opacity: 0;
  /* 5px, not the previous 6px — brief: "move outgoing content upward by
     no more than 5px... fade the new content in from approximately 5px
     below". A single shared "hidden" offset (rather than a separate
     enter/exit direction) is the same proven crossfade technique already
     used sitewide for this exact stacked-grid-cell pattern; the visual
     result still reads as "enter from below, dominant motion stays the
     dotted sweep" at this small a distance. */
  transform: translateY(5px);
  transition: opacity 380ms var(--cb-ease-standard), transform 380ms var(--cb-ease-standard);
  pointer-events: none;
}

.process__ring-stage.is-current {
  opacity: 1;
  transform: translateY(0);
}

/* The left column's crossfading title/paragraph — built by main.js's
   initProcess as four stacked, grid-overlapping children (same technique
   as .process__ring-stage above), never text-content-swapped in place.
   Now the SAME 5px/380ms treatment as .process__ring-stage (was an
   opposite-direction -6px/400ms) — brief: "coordinate the left-side
   changing content and the inner-circle content" at each threshold, read
   as one unified transition rather than two panels moving in different
   directions.

   Deliberately NOT sized to the current stage's own height (a previous
   pass did that, to close up the visual gap under a short summary) — this
   shared grid cell's natural behaviour, sizing itself to the TALLEST of
   the four stacked variants, keeps this whole block at one fixed height
   regardless of which stage is current, so nothing below it in normal
   flow ever jumps vertically as the crossfade cycles. Since every variant
   is `align-self:start` within that constant-height cell, the title's own
   position never moves either — reserving height for the LONGEST summary
   is achieved here by simply not fighting the grid's own default sizing.
   (The "Discover our approach" CTA that used to sit directly below this
   stack, and originally motivated this fixed-height behaviour, has since
   been removed — the reasoning still holds regardless.) */
.process__stage-stack {
  position: relative;
  display: grid;
}

.process__stage {
  grid-area: 1 / 1;
  align-self: start;
  opacity: 0;
  transform: translateY(5px);
  transition: opacity 380ms var(--cb-ease-standard), transform 380ms var(--cb-ease-standard);
  pointer-events: none;
}

.process__stage.is-current {
  opacity: 1;
  transform: translateY(0);
}

/* The scroll-linked dot sweep can't sensibly be "reduced" without removing
   the section's entire mechanism (it's directly tied to the user's own
   scroll input, not autoplay, so there's nothing here to disable the way
   an autoplaying animation would be) — this only turns off the discrete
   text-crossfade transition, an instant swap instead of a timed fade,
   matching the same courtesy every other animated section on this page
   extends to this preference. */
@media (prefers-reduced-motion: reduce) {
  .process__ring-stage,
  .process__stage {
    transition: none;
  }
}

/* BUREAU ACCESS PROMO — sits between Process and FAQ. Same
   padding-block(--cb-section-gap) pattern as .faq immediately below —
   the gap to both neighbours is already equal, so no divider is added.
   Intro column centred and capped the same way as .faq__intro (matching
   established rhythm, not a bespoke width). */
.bureau-access-promo {
  position: relative;
  padding-block: var(--cb-section-gap);
}
.bureau-access-promo__container {
  max-width: 75rem;
  margin-inline: auto;
}
.bureau-access-promo__intro {
  max-width: 42.5rem;
  margin-inline: auto;
  text-align: center;
}
/* Same heading typography as every other section on this page (.faq__heading
   etc.) — one shared convention, not a bespoke scale for this section. */
.bureau-access-promo__heading {
  margin: var(--cb-space-3) 0 0;
  font-family: var(--cb-font-sans);
  font-weight: var(--cb-weight-semibold);
  font-size: var(--cb-text-page-heading-size);
  line-height: var(--cb-text-page-heading-leading);
  letter-spacing: var(--cb-text-page-heading-tracking);
  color: var(--cb-color-ink);
}
.bureau-access-promo__supporting {
  margin: var(--cb-space-4) 0 0;
  color: var(--cb-color-text-secondary);
}
/* Quiet CTA directly under the intro copy — the sitewide .cta/.cta__label/
   .cta__arrow component (components.css), not a boxed button. Uses the
   shared --cb-copy-cta-gap token (tokens.css) — 24px mobile/tablet, 28px
   desktop — so the CTA reads as deliberately separated from the paragraph
   above it, not tucked underneath it. Two-class compound selector, not just
   .bureau-access-promo__cta: components.css's own shared .cta rule sets
   `margin: -0.5em -0.25em` (the transparent-padding/negative-margin bigger-
   tap-target technique) as a single-class selector loaded AFTER this
   stylesheet, so at equal specificity it silently wins and overrides
   margin-top back to roughly -8.5px. Same fix already used by
   .feature-panel__copy .feature-panel__cta above. */
.bureau-access-promo__intro .bureau-access-promo__cta {
  margin-top: var(--cb-copy-cta-gap);
}

/* CARDS — plain content, not navigation (no arrows, no per-card CTA, no
   hover-as-link treatment), same convention as
   .expertise-capabilities__card. Paper background + a subtle Line border
   (not the grey --expertise-surface fill) per brief: "Paper background,
   subtle line border, no heavy shadow". */
.bureau-access-promo__cards {
  margin: var(--cb-space-7) auto 0;
  max-width: 75rem;
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--cb-space-4);
}

/* Tablet: 2x2 — same breakpoint .expertise-capabilities__grid already uses
   for its own 2-column fallback. */
@media (min-width: 40rem) {
  .bureau-access-promo__cards {
    grid-template-columns: repeat(2, 1fr);
  }
}

/* Wide desktop: a compact, centred 3x2 grid for the six cards — narrower
   than a full 4-column row would be, so it reads as a considered block
   rather than stretching edge to edge. */
@media (min-width: 80rem) {
  .bureau-access-promo__cards {
    max-width: 62.5rem;
    grid-template-columns: repeat(3, 1fr);
    gap: var(--cb-space-5);
  }
}

/* Same light-grey surface as the Featured Project panel's own left/text
   side (.feature-panel, above) — color-mix(ink 2%, paper), reused exactly,
   not a new grey. .feature-panel has no border against that surface, and
   compared side by side the hairline --cb-color-line border read as
   redundant/fussy against the same tinted surface here too, so it's
   dropped in favour of that more refined (already-approved) treatment.
   Still no shadow. */
.bureau-access-promo__card {
  background: color-mix(in srgb, var(--cb-color-ink) 2%, var(--cb-color-paper));
  border-radius: var(--cb-radius-button);
  padding: var(--cb-space-5) var(--cb-space-4);
  text-align: center;
  /* Column flex, not just block flow — lets the title zone below reserve a
     fixed height via min-height while the icon above and description below
     stay in normal, top-aligned document flow, so every card's icon/title/
     description zones start at the same Y position regardless of how many
     lines a given title wraps to. */
  display: flex;
  flex-direction: column;
  align-items: center;
}

/* Small, monochrome, Graphite — never a filled/coloured icon container.
   28px keeps it a quiet supporting mark above the title, not a "feature
   icon" the card is built around. Identical padding-top + a fixed icon size
   on every card is what makes every icon's top edge land on the same Y
   position across the row — no per-card centring needed. */
.bureau-access-promo__card-icon {
  width: 28px;
  height: 28px;
  color: var(--cb-color-graphite);
  flex-shrink: 0;
}

/* Reserves a consistent two-line-tall zone on every card (the longest
   titles — "Files & deliverables", "Agreements & invoices" — wrap to two
   lines at this width) so a one-line title like "Reviews & approvals"
   still occupies the same vertical space as a two-line one. Text stays in
   normal top-aligned flow inside that reserved box (deliberately not
   flex/centred within itself), so the description below always starts at
   the same Y across all six cards regardless of title line count. Uses
   the type-scale tokens directly (size x leading x 2) rather than a fixed
   pixel value, so it stays correct if the fluid subheading size changes. */
.bureau-access-promo__card-title {
  margin: var(--cb-space-3) 0 0;
  min-height: calc(var(--cb-text-subheading-size) * var(--cb-text-subheading-leading) * 2);
  font-family: var(--cb-font-sans);
  font-weight: var(--cb-weight-medium);
  font-size: var(--cb-text-subheading-size);
  line-height: var(--cb-text-subheading-leading);
  letter-spacing: var(--cb-text-subheading-tracking);
  color: var(--cb-color-ink);
}

.bureau-access-promo__card-description {
  margin: var(--cb-space-2) 0 0;
  font-size: var(--cb-text-body-size);
  line-height: var(--cb-text-body-leading);
  color: var(--cb-color-text-secondary);
}

/* Below the row where cards individually stack (see .bureau-access-promo__
   cards' own 40rem tier above), the rigid cross-card title alignment is no
   longer doing anything useful — each card's title can size to its own
   content again. */
@media (max-width: 39.9375rem) {
  .bureau-access-promo__card-title {
    min-height: 0;
  }
}

/* FAQ — a single centred column between Process and Contact, deliberately
   NOT a two-column layout like every section around it. padding-block is
   var(--cb-section-gap) — this section's share of the shared
   2×var(--cb-section-gap) section-to-section gap on BOTH sides at once
   (padding-block sets top and bottom to the same value), matching
   .process's own margin-top/bottom and .site-footer's own padding-top.
   Still equal on both sides of the boundary with .contact, so nothing here
   needs an actual divider. */
.faq {
  position: relative;
  padding-block: var(--cb-section-gap);
}

.faq__container {
  max-width: 75rem;
  margin-inline: auto;
}

/* 42.5rem (680px) — inside the brief's suggested 650-720px range. Centred
   independently of .faq__container's own (much wider) max-width, same
   established pattern as .selected-work__intro's own centred max-width. */
.faq__intro {
  max-width: 42.5rem;
  margin-inline: auto;
  text-align: center;
}

/* 60rem (960px) — inside the brief's suggested 900-980px range. Centred as
   a block within the wider section, but its own contents (each question
   row) stay left-aligned — brief: "the accordion itself should remain
   left-aligned for readability" — text-align is deliberately not set here,
   only on .faq__intro above. border-top is this list's own opening
   divider; every .faq__item below supplies its own trailing one, so the
   full set reads as one continuous rule cutting each row from the next. */
/* Intro-to-accordion gap down from space-7 (64px) to space-6 (48px) — a
   25% reduction, part of the same compaction pass as .faq's own reduced
   padding-block above. */
.faq__list {
  margin: var(--cb-space-6) auto 0;
  max-width: 60rem;
  list-style: none;
  padding: 0;
  border-top: var(--cb-border-width) solid var(--cb-color-border);
}

.faq__item {
  border-bottom: var(--cb-border-width) solid var(--cb-color-border);
}

.faq__item-heading {
  margin: 0;
}

/* The entire question row is the clickable target (brief) — a real
   <button>, reset to plain text/no background/no border so it reads as
   part of the list, not a UI control; base.css's shared button:focus-visible
   rule already covers its keyboard focus outline, so nothing new is needed
   there. Block padding doubles as the divider-to-divider spacing AND the
   touch target on mobile, without a separate min-height rule — down from
   space-4 (24px) to 1.25rem (20px) this pass, a ~17% reduction (inside the
   brief's requested 15-20%); still comfortably touch-sized, just tighter. */
.faq__trigger {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--cb-space-4);
  width: 100%;
  margin: 0;
  padding-block: 1.25rem;
  background: none;
  border: none;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
}

/* Rotates on the button's own aria-expanded state, not a separate JS-
   toggled class — one fewer thing that could drift out of sync with the
   accessible state main.js is already responsible for setting. Brief:
   "arrow rotates smoothly", "arrows, not plus icons" — a plain glyph
   (index.html), rotated, not a new icon asset. */
.faq__arrow {
  flex-shrink: 0;
  transition: transform 300ms var(--cb-ease-standard);
}

.faq__trigger[aria-expanded="true"] .faq__arrow {
  transform: rotate(180deg);
}

/* The CSS grid-template-rows 0fr/1fr technique — animates a height that's
   otherwise unknowable in advance (the answer's own natural height) without
   any JS measuring scrollHeight. `inert` (index.html; toggled by main.js's
   initFaq, not a separate class) is the single source of truth for both
   this collapsed visual state AND removing the closed answer from
   keyboard/screen-reader reach — the [inert] attribute selector below is
   the only thing keying off it. Not `transition: all` — only the two
   properties that actually change. */
.faq__panel {
  display: grid;
  grid-template-rows: 0fr;
  opacity: 0;
  transition: grid-template-rows 300ms var(--cb-ease-standard), opacity 300ms var(--cb-ease-standard);
}

.faq__panel:not([inert]) {
  grid-template-rows: 1fr;
  opacity: 1;
}

/* The grid ITEM inside .faq__panel needs its own overflow:hidden for the
   0fr collapse above to actually clip the content rather than overflow it
   — .faq__answer is that one child, so this lives here rather than on a
   wrapper that would otherwise serve no other purpose. Padding (not
   margin) for the same reason .form-field's own spacing avoids margin
   collapsing oddities inside a non-block-formatting-context ancestor;
   top pulls it slightly away from the question above once open, bottom
   gives it room before the item's own trailing divider — down slightly
   this pass (1.25rem/20px, was space-4/24px), matching .faq__trigger's own
   reduced padding above rather than a separate value. */
.faq__answer {
  overflow: hidden;
  margin: 0;
  padding-top: var(--cb-space-2);
  padding-bottom: 1.25rem;
}

/* Scroll-linked and timer-driven motion elsewhere on this page each have
   their own reduced-motion override right next to their own rules, not one
   shared catch-all — this is this component's own. */
@media (prefers-reduced-motion: reduce) {
  .faq__arrow,
  .faq__panel {
    transition: none;
  }
}

/* Contact — editorial intro (left) + form (right), stacked below the
   50.0625rem breakpoint used everywhere else in this file for a two-column
   split. Own max-width (not the plain, unbounded .container edge-inset
   padding) for the same reason .selected-work__grid needed one: two real
   input columns plus an intro column would otherwise stretch with the
   viewport well past a comfortable form width at 1440px+. */
.contact {
  position: relative;
  /* This section's share of the shared 2×var(--cb-section-gap)
     section-to-section gap on both sides at once (padding-block). */
  padding-block: var(--cb-section-gap);
}

.contact__container {
  max-width: 75rem;
  margin-inline: auto;
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--cb-space-7);
}

@media (min-width: 50.0625rem) {
  .contact__container {
    /* 34-38% per brief — minmax(0, ...) so the column can still shrink below
       its intrinsic content width instead of forcing the row wider (plain
       fractions/percentages on a grid track default to a min-width:auto
       floor, the same overflow trap .hero__content's own comment already
       documents for flex). align-items:start keeps the intro's own top
       flush with the form's top, per brief, rather than the grid's default
       stretch. */
    grid-template-columns: minmax(0, 36%) 1fr;
    align-items: start;
    gap: var(--cb-space-8);
  }
}

.form-row {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--cb-space-4);
}

.form-row:not(:first-child) {
  margin-top: var(--cb-space-4);
}

/* Own, smaller breakpoint than the section's own intro/form split — once the
   section stacks (mobile, and tablet widths too narrow for two columns), the
   form spans nearly the full viewport width, which comfortably fits two
   fields side by side well before 50.0625rem. */
@media (min-width: 30rem) {
  .form-row {
    grid-template-columns: 1fr 1fr;
  }
}

.form-field--full {
  margin-top: var(--cb-space-4);
}

/* Footer — the site's one deliberately inverse surface (brief: "a confident
   ending to the landing page"), Ink background/Paper text via the same
   semantic tokens every other section already reads its own (paper-on-ink)
   colours from, not new hex values. Two stacked rows (identity / legal)
   rather than the previous single narrow line — mobile-first: this base
   rule is the fully-stacked mobile layout; the ≥50.0625rem tier below
   restores each row to a horizontal line. padding-bottom (space-5 = 32px)
   is unrelated to inter-section spacing (nothing follows the footer) and
   still sits inside an earlier brief's own "approximately 32-40px" — this
   is genuine footer padding, explicitly not to be reduced with the rest of
   the page's section spacing. padding-top is var(--cb-section-gap) — the
   footer's share of the shared 2×var(--cb-section-gap) section-to-section
   gap (.contact's own padding-bottom is the other half). */
.site-footer {
  background: var(--cb-color-bg-inverse);
  color: var(--cb-color-text-inverse);
  /* A dedicated, slightly smaller value at each tier — not var(--cb-
     section-gap) itself, which is shared sitewide for the gap BETWEEN two
     sections and must stay untouched for those. A footer is a terminus,
     not "the next chapter", so it doesn't need quite that much clearance
     above its own top row — a refinement pass: "move the logo/columns
     slightly upward... the top row feels dropped down". Reusing the same
     50.0625rem/80rem breakpoints as --cb-section-gap's own tiers below. */
  padding-top: 36px;
  padding-bottom: var(--cb-space-5);
}

@media (min-width: 50.0625rem) {
  .site-footer {
    padding-top: 48px;
  }
}

@media (min-width: 80rem) {
  .site-footer {
    padding-top: 72px;
  }
}

/* Mobile-first: stacked column — wordmark then .site-footer__links (itself
   stacked into Connect above Explore at this width) — the ≥50.0625rem tier
   below switches both to a row, with .site-footer__links becoming one
   tight, right-aligned cluster rather than a full-width grid. */
.site-footer__top {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--cb-space-6);
}

/* Connect + Explore, grouped as one unit — a refinement pass reverting away
   from a 6/3/3 grid spanning the full row: that let each column's own
   narrow text content stop well short of the row's right edge, reading as
   dead space rather than "aligned to the page gutter". Grouping them keeps
   the pair's own combined width tight and lets .site-footer__top's
   justify-content:space-between (below) push that whole group flush against
   the right gutter instead. */
.site-footer__links {
  display: flex;
  flex-direction: column;
  gap: var(--cb-space-6);
  width: 100%;
}

/* .site-footer .cta (a 2-class selector), not .site-footer__email alone --
   components.css's own unconditional .cta rule (color:ink) loads AFTER this
   file, so an equal-specificity override here would silently lose the
   cascade regardless of which one reads more "specific to this component";
   same gotcha already documented elsewhere in this file. The extra
   specificity from the .site-footer ancestor class is what guarantees this
   wins. Covers both .site-footer__email and .site-footer__social (the
   Instagram placeholder) in one rule -- both carry the shared .cta class. */
/* Small/legal-link size and regular weight, not .cta's own body-size/
   semibold — a refinement pass: that combination read as too large and
   heavy next to the deliberately quiet Explore column beside it. Bundled
   into this same higher-specificity rule (not a separate
   .site-footer__email/.site-footer__social selector) for exactly the
   reason documented below: a plain single-class selector would tie with
   components.css's own .cta and lose, since that file loads after this
   one. */
.site-footer .cta {
  /* Muted, not full-opacity Paper — "the same typographic spirit as the
     legal links" (which use a 55%-Paper mix): 78% keeps Connect reading as
     real navigation, still clearly a step above the legal row, while
     losing the previous full-brightness heaviness. Brightens to full Paper
     on hover/focus below, for feedback. */
  color: color-mix(in srgb, var(--cb-color-paper) 78%, transparent);
  font-size: var(--cb-text-small-size);
  font-weight: var(--cb-weight-regular);
}

.site-footer .cta:not(.is-placeholder):hover,
.site-footer .cta:not(.is-placeholder):focus-visible {
  color: var(--cb-color-text-inverse);
}

/* Stacks the Connect column's two links (email, Instagram) — both are
   .cta-classed (inline-flex by default), so this gives them a predictable
   vertical rhythm regardless of how wide either one's own content is. */
.site-footer__connect-links {
  display: flex;
  flex-direction: column;
  gap: var(--cb-space-3);
}

.site-footer__group-label {
  margin: 0 0 var(--cb-space-3);
  font-family: var(--cb-font-sans);
  font-weight: var(--cb-weight-medium);
  font-size: var(--cb-text-label-size);
  line-height: var(--cb-text-label-leading);
  letter-spacing: var(--cb-text-label-tracking);
  text-transform: uppercase;
  /* Paper at reduced opacity -- brief's own "secondary text" rule, built
     from the same Paper token rather than a new colour. */
  color: color-mix(in srgb, var(--cb-color-paper) 55%, transparent);
}

/* Same gap as .site-footer__connect-links beside it (Connect and Explore
   previously used different gaps — space-2 vs space-3 — so rows drifted
   out of alignment further down the list even where row one happened to
   line up). .site-footer .nav-link below gets .cta's own padding/
   negative-margin pair so both columns' items share an identical
   margin-box geometry — that's what makes every row align by
   construction rather than by a manually tuned offset. */
.site-footer__link-list {
  display: flex;
  flex-direction: column;
  gap: var(--cb-space-3);
  margin: 0;
  padding: 0;
  list-style: none;
}
/* display: flex, not the default list-item — a list-item box's height
   includes a "strut" sized to its OWN inherited font (17px body text,
   never overridden on the <li> itself) even though its only content is
   a 14px .nav-link; that mismatched strut was exactly what threw off
   the negative-margin cancellation trick below and made row spacing
   drift further apart on every subsequent row. A flex container has no
   such strut — its height comes purely from the child's own margin box. */
.site-footer__link-list li {
  display: flex;
}

.site-footer .nav-link {
  /* Muted, matching .site-footer .cta beside it and "the same typographic
     spirit as the legal links" a refinement pass asked for — full-opacity
     Paper read as too prominent/heavy for what's meant to be a quiet
     Explore column, not a competing focal point. */
  color: color-mix(in srgb, var(--cb-color-paper) 78%, transparent);
  /* Same small size as the legal links below and the Connect links beside
     it — a refinement pass: the previous body-size Explore column read as
     too large/heavy next to CONNECT/EXPLORE's own small uppercase labels. */
  font-size: var(--cb-text-small-size);
  /* Same padding/negative-margin pair as .cta (components.css) — not for
     the bigger hit-target (nice side effect), but so each Explore row's
     margin-box geometry exactly matches each Connect row's. Two
     structurally different components (.cta's inline-flex-with-cancelled-
     padding vs a plain <li><a>) previously rendered at different
     effective heights, so rows drifted out of alignment row by row even
     when gap values matched. */
  display: inline-flex;
  align-items: center;
  padding: 0.5em 0.25em;
  margin: -0.5em -0.25em;
}

/* Real (non-placeholder) footer nav links get their own subtle feedback --
   .nav-link has none by default sitewide (the header's own real link,
   "What We Do", relies only on the browser's default). A quiet underline
   fade reads as deliberate here without introducing a new visual language;
   duration/easing reuse the sitewide motion tokens, never `transition: all`. */
.site-footer .nav-link:not(.is-placeholder) {
  transition: opacity var(--cb-duration-fast) var(--cb-ease-standard),
    color var(--cb-duration-fast) var(--cb-ease-standard);
}

.site-footer .nav-link:not(.is-placeholder):hover,
.site-footer .nav-link:not(.is-placeholder):focus-visible {
  color: var(--cb-color-text-inverse);
  text-decoration: underline;
}

.site-footer__bottom {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--cb-space-3);
  margin-top: var(--cb-space-4);
  padding-top: var(--cb-space-3);
  /* Small/fixed 14px, not the ambient body size the copyright/legal line
     inherited before -- brief: "make the entire bottom row smaller...
     more subtle and secondary than the main footer content." Set once
     here so .site-footer__meta and .site-footer__legal a both inherit it
     rather than repeating the value on each. */
  font-size: var(--cb-text-small-size);
  /* 16%, inside the brief's own "approximately 14-18%" range. */
  border-top: var(--cb-border-width) solid color-mix(in srgb, var(--cb-color-paper) 16%, transparent);
}

.site-footer__meta {
  margin: 0;
  color: color-mix(in srgb, var(--cb-color-paper) 55%, transparent);
}

.site-footer__legal {
  display: flex;
  flex-wrap: wrap;
  gap: var(--cb-space-2) var(--cb-space-4);
}

/* Selector also matches the "Cookie Preferences" control, a real <button>
   (it opens the shared cookie-preferences panel rather than navigating —
   same "real control, not a fake link" convention already used sitewide),
   not the <a> "Privacy Policy"/"Terms of Use" links beside it. The extra
   properties on this rule only reset a button's own browser-default
   appearance/font/padding/border so it renders identically to those two
   links — no typography or spacing value here differs from what this
   selector already applied. */
.site-footer__legal a,
.site-footer__legal button {
  appearance: none;
  border: none;
  background: none;
  padding: 0;
  cursor: pointer;
  font: inherit;
  color: color-mix(in srgb, var(--cb-color-paper) 55%, transparent);
  text-decoration: none;
  transition: color var(--cb-duration-fast) var(--cb-ease-standard);
}

.site-footer__legal a:hover,
.site-footer__legal a:focus-visible,
.site-footer__legal button:hover,
.site-footer__legal button:focus-visible {
  color: var(--cb-color-text-inverse);
  text-decoration: underline;
}

/* base.css's sitewide :focus-visible outline is Ink -- correct everywhere
   else on the page's own Paper background, but invisible against this one
   inverse (Ink-background) surface. Overridden to Paper here only; width/
   offset/radius are untouched, inherited from that same shared rule.
   Higher specificity (.site-footer ancestor class) is what guarantees this
   wins regardless of the two files' own load order. */
.site-footer a:focus-visible,
.site-footer button:focus-visible {
  outline-color: var(--cb-color-text-inverse);
}

@media (min-width: 50.0625rem) {
  /* Two direct children now (wordmark, .site-footer__links) — flex-start
     (not centre) keeps the logo reading as "isolated" at a clear position
     rather than floating vertically centred against its taller neighbour;
     justify-content:space-between pushes the links group flush against the
     row's own right edge, aligned to the page gutter, whatever its own
     (now much narrower) combined width happens to be. */
  .site-footer__top {
    flex-direction: row;
    align-items: flex-start;
    justify-content: space-between;
  }

  /* Connect and Explore sit close together as one tight cluster rather
     than each spanning a wide grid cell — a fixed-ish, moderate gap (not a
     wide clamp) is what keeps this reading as "one group", not two
     independent columns. */
  .site-footer__links {
    flex-direction: row;
    width: auto;
    gap: clamp(2.5rem, 5vw, 4rem);
  }

  .site-footer__bottom {
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
  }
}
