/* The header and footer, on a page that is not this theme's.
   ===================================================================

   WHAT THIS FILE IS FOR. The Vivid header and footer are going onto the
   ~450 pages that stay on the 2024 theme. Those pages render from
   `CINC SystemJune-2024/`, so they load that theme's stylesheet and know
   nothing about this one — no tokens, no reset, no .btn, no .container.

   The modules cannot carry that themselves: module.css holds the chrome's
   own geometry and leans on the theme underneath it for the button, the
   container, the visually-hidden helper and every token in the palette.
   So something has to bridge the two, and this is it.

   WHY NOT JUST AUTOLOAD main.css. That is what header.module and
   footer.module used to declare, and it is the reason this file exists.
   main.css styles html, body, p, a, h1-h6, ul, ol, img, main and section at
   ELEMENT level, and the 2024 theme has 14 rules touching .container and 23
   touching .btn. Autoloading it does not add a header to those pages — it
   restyles all of them, in one upload, with no way to see it coming.
   Measured before the swap, not guessed at.

   So: the tokens go on the page unscoped, because a custom property has to
   be inherited from :root to reach anything, and NOTHING ELSE DOES. Every
   rule below is scoped to the three boxes the chrome actually occupies.

   THE SCOPE IS :is(), NOT :where(), AND THAT IS THE WHOLE DESIGN.

   :where() would have been faithful — zero specificity, so every rule keeps
   exactly the weight it has on a Vivid page. It is also wrong here, because
   `css-gdco/elements/_buttons.css` carries a bare `.btn` at (0,1,0):

       .btn { text-transform: uppercase; font-size: 13px; letter-spacing: .1em;
              border-radius: 40px; padding: 1.2em 2.4em; display: inline-block; }

   That is the exact tracked-caps micro-button that _buttons.css in this theme
   spends three paragraphs having removed. Tie it on specificity and the
   winner is decided by whichever stylesheet HubSpot happens to inject second
   — which is not something to leave to chance on 450 pages.

   :is() takes the specificity of its heaviest argument, so the scope adds one
   class and the ladder comes out right on purpose:

       old theme            .btn                                    (0,1,0)
       THIS FILE            :is(.site-header, …) .btn               (0,2,0)  wins
       header module.css    .site-header .pill .nav-actions .btn    (0,4,0)  still wins

   The chrome sits above the old theme and below its own modules, which is
   the only order that leaves both intact.

   ON A VIVID PAGE this file loads alongside main.css and changes nothing:
   the rules are the same rules, from the same includes, one class heavier.
   Duplicated :root tokens resolve to identical values. The cost is payload,
   which is cached and minified; the benefit is that there is exactly one
   definition of a button in this theme.

   NOTHING HERE IS COPIED. Every block below pulls in the file that already
   owns those rules, so the chrome cannot drift away from the theme the way a
   hand-transcribed copy would. The two exceptions are marked and explained
   where they appear — both are cases where the source file contains something
   that is illegal once nested.

   DO NOT WRITE AN INCLUDE TAG IN PROSE ANYWHERE IN THIS FILE. CSS comment
   syntax means nothing to HubL: it parses the whole file before a comment is
   anything but characters, so a tag named in a sentence is a real tag. This
   paragraph used to spell one out, with no path in it, which is a malformed
   include that fails at UPLOAD — after preflight and the local preview have
   both passed, because both read the CSS at a point where HubL would already
   have run. elements/_buttons.css carries the same warning about a set tag,
   for the same reason and from the same mistake.

   _helper.css brings ~160 generated colour utilities in with it, scoped to
   the chrome, where nothing uses them. That is a deliberate trade: this file
   needs .srt, and copying nine lines of visually-hidden CSS to avoid some
   minified kilobytes is the exact bargain the paragraph above refuses. */


/* ============ 0. THE TYPEFACE ============
   The 2024 base layouts ask Google for Montserrat and nothing else, so on
   those pages Inter is simply not on the machine. The token's fallback stack
   would carry it — 'Inter', system-ui, … — but not intact: the type scale is
   built on the intermediate weights 450, 550, 650 and 750, which exist in
   Inter Variable and in none of the system faces. A header set at 550 lands
   on 500 or 600 and the nav stops matching the comps.

   Inter only. The chrome does not use Playfair — grepped both module.css
   files: one font-family declaration between them, and it is var(--inter).
   Asking for the display face as well would be two font files nobody reads.

   An @import has to precede every rule in the file, which is why this sits
   above the token block rather than beside the fonts in layouts/base.html.
   On a Vivid page it is a no-op: base.html has already requested the same
   family and the browser serves it from cache. */
@import url("https://fonts.googleapis.com/css2?family=Inter:opsz,wght@14..32,100..900&display=swap");


/* ============ 1. TOKENS ============
   ON THE CHROME'S OWN ROOTS, NOT ON :root, and the reasoning is long enough
   that it lives where the tokens do — see the note at the top of
   generic/_variables.css. The short version: the 2024 theme has 32 custom
   properties on :root and the two vocabularies deliberately overlap, so a
   second :root block either loses the injection-order race (measured: --rad
   came back as the old theme's 16px inside the Vivid header) or wins it and
   redefines --white, --gray700 and --cincGreen for 450 pages of someone
   else's content.

   Custom properties inherit, so declaring them here reaches every descendant
   of the chrome and nothing else on the page.

   .skip-link IS IN THE LIST and has to be: it is emitted as a SIBLING of
   .site-header, not a child, so it inherits from <body> like any other
   element on the host page. Left out, it reads the 2024 theme's --rad and
   --surface and draws the wrong corner on the wrong ground. */

/***********************************************/
/* CSS @imports must be at the top of the file */
/* Add them above this section                 */
/***********************************************/

/*****************************************/
/* Vivid design tokens                   */
/* Source: cinc-vivid-deck-8112026.pdf   */
/*                                       */
/* Three layers, in order:               */
/*   1. Palette — raw brand values       */
/*   2. Roles   — what components use    */
/*   3. Compat  — legacy CINC names      */
/*                                       */
/* Components reference ROLE tokens only.*/
/* Never reach past a role to a palette  */
/* value, and never hardcode a hex.      */
/*****************************************/

/* WHERE THE TOKENS LAND, and why that is a parameter now.

   On a Vivid page this is :root and always has been. css/chrome.css — the
   sheet that carries the header and footer onto the 2024 theme's pages — sets
   this to the chrome's own roots instead, and it has to, in both directions:

     THE OLD THEME WOULD WIN. It declares 32 custom properties of its own on
     :root, and the two vocabularies overlap by design — the compat layer at
     the foot of this file exists precisely because ported modules still ask
     for --white, --gray700, --cincGreen. Two :root blocks are the same
     specificity, so which palette the chrome reads comes down to whichever
     stylesheet HubSpot injected second. Measured on the collision harness
     before this changed: --rad resolved to the 2024 theme's 16px inside the
     Vivid header, and the skip link drew a 16px corner instead of a 12px one.

     AND WE WOULD WIN SOMEWHERE WE SHOULD NOT. Put this on :root of a 2024
     page and every one of those overlapping names is redefined for the whole
     document — a 450-page restyle delivered through a variable block, which
     is the exact failure chrome.css was written to avoid.

   Declared on the chrome's roots, the tokens inherit down through the header
   and footer and reach nothing else. Nobody has to win a race.

   Do not read this as a general-purpose knob. There is one caller, and a
   second scope should be a second reason, argued in a comment like this. */


.sticky-nav, .site-header, .site-footer, .skip-link {

	/* ============ 1. PALETTE ============ */

	/* Six core colors (deck p28) */
	--deep-green:   #002D28;  /* Foundation, authority, premium contrast */
	--evergreen:    #006A52;  /* Accessible functional green. LIGHT SURFACES ONLY */
	--vivid-green:  #00A254;  /* Recognition, action, momentum */
	--lime:         #B6E75A;  /* Intelligence at work. DARK SURFACES ONLY */

	/* The dot pattern's own two greens, sampled from the supplied SVG's
	   gradient stops. Deliberately not --vivid-green and --evergreen: the
	   artwork uses #00AD6D and #22584E, which are close to but not the same
	   as the brand pair, and matching the file matters more here than
	   folding a decorative asset into the palette. */
	--dot-lit:      #00AD6D;
	--dot-dim:      #22584E;

	/* The timeline curve's own green, sampled from the supplied SVG's stroke.
	   Same reasoning as the dot pair — but here the hair between it and
	   --vivid-green does real work: #00A44A is 4.56:1 on Deep Green against the
	   token's 4.48:1, so the 12px day label under the head clears AA on this
	   one and would fail on the other. Not interchangeable. */
	--thread-green: #00A44A;
	--warm-white:   #FBFAF7;  /* Primary brand canvas — never pure white */
	--pure-white:   #FFFFFF;  /* Type and chrome ON a dark or green surface, and
	                             the one raised surface. The deck's "never pure
	                             white" is a rule about the CANVAS; using it for
	                             type going the other way buys contrast rather
	                             than spending it.

	                             NOT called --white: the compat layer at the
	                             foot of this file already maps --white to
	                             Warm White for ported CINC-system modules,
	                             and being declared later it would win. */
	--ink:          #171B19;  /* Primary typography — never pure black */

	/* Working neutrals (deck p30) */
	--cloud:          #F3F5F2;  /* Quiet cards and alternating surfaces */
	--community-mist: #E8F1EC;  /* Green-adjacent panels and diagrams */
	--stone:          #D6DCD8;  /* DECORATIVE dividers only — never meaning */
	--stone-deep:     #7F9185;  /* Functional borders and input outlines */
	--slate:          #5E6863;  /* Secondary text on light — 5.53:1 AA */

	/* NOT IN THE DECK. Sand is the warm neutral the comps alternate with Warm
	   White — the testimonial band, the promo card fill, the lower half of
	   page 2 — and six modules had measured it before it was named. Cloud
	   stood in for it and is the wrong temperature: #F3F5F2 is cool and a
	   shade lighter, so a sand band drawn in Cloud reads as a printing error
	   next to Warm White rather than as a second ground.

	   It is a CANVAS, and every role that lands on it clears AA: ink 15.24:1,
	   Slate 5.03:1, Evergreen 5.74:1, Deep Green 13.00:1. Ask Jay to fold it
	   into the deck at the next revision — until then this comment is the
	   only place its provenance is written down. */
	--sand:           #F2EFE7;  /* Warm alternating band — see --surface-warm */

	/* Expressive secondaries (deck p34)
	   Each has a fill, a type-safe variant, and a fixed mist tint.
	   Use the fixed tints — never opacity. */
	--community-blue:      #8FC6D8;
	--community-blue-type: #32768E;
	--blue-mist:           #E8F4F7;

	--community-clay:      #C9785B;
	--community-clay-type: #A45236;
	--clay-mist:           #F4E7E2;

	--golden-ochre:        #D6A247;
	--golden-ochre-type:   #8E6520;
	--ochre-mist:          #F7EEDC;

	--vivid-violet:        #6658E8;
	--vivid-violet-type:   #5A4CCD;
	--violet-mist:         #ECE9FF;


	/* ============ 2. ROLES ============ */

	/* --- The three surfaces (deck p66) --- */
	/* Never stack two announcing or declaring surfaces back to back. */
	--surface:            var(--warm-white);   /* WORKING — the default, 45–60% of any page */
	--surface-quiet:      var(--cloud);        /* Quiet cards, alternating bands — COOL */
	--surface-warm:       var(--sand);         /* The comps' warm alternating band. Pairs
	                                              with quiet: same job, other temperature.
	                                              Sand is warmer AND darker than Warm White,
	                                              so it reads as a band beside it; Cloud is
	                                              cooler and does not. */
	--surface-panel:      var(--community-mist);
	--surface-raised:     var(--pure-white);             /* The one sanctioned pure white: a card
	                                              lifted ABOVE the Warm White canvas. Cloud
	                                              is darker than the canvas so it cannot
	                                              lift; Warm White on Warm White is invisible.
	                                              Never as a page ground. */
	--surface-announcing: var(--vivid-green);  /* Section openers, momentum. Ink type. */
	--surface-declaring:  var(--deep-green);   /* Statements. Warm White type + Lime accent. */

	/* --- Type --- */
	--ink-primary:   var(--ink);         /* 16.67:1 on Warm White — AAA */
	--ink-secondary: var(--slate);       /* 5.53:1 on Warm White — AA */
	/* Pure white, not Warm White. On Deep Green it takes 14.31:1 to 14.94:1,
	   and on Vivid Green 3.20:1 to 3.34:1 — small, but there is no reason to
	   give contrast away on the surfaces that need it most. */
	--ink-inverse:   var(--pure-white);       /* Type on declaring/announcing surfaces */
	/* Secondary type on a DECLARING surface — the inverse of --ink-secondary,
	   and the same job: supporting copy that should sit back from the
	   headline without dropping out of it. 9.98:1 on Deep Green, so it is AA
	   at any size.

	   A mix rather than a palette value because the comps' choice, Stone
	   #D6DCD8, is --line — a decorative divider, not a type colour — and
	   reaching for it here would put a divider token on running text. Mixed
	   against Deep Green rather than made transparent so it composites the
	   same over the band, over a photograph and over a scrim. statement-cards
	   and partner-panel both derived this expression before it had a name. */
	--ink-inverse-secondary: color-mix(in srgb, var(--pure-white) 80%, var(--deep-green));
	--ink-deep:      var(--deep-green);  /* Deep Green as type on light — 14.31:1 AAA.
	                                        Distinct from --ink-accent: this is a near-black
	                                        with a green cast for headings, not the
	                                        functional green. The comps use it for card and
	                                        column headings throughout. */
	--ink-accent:    var(--evergreen);   /* Green type on LIGHT surfaces — 6.32:1 AA.
	                                        Never Vivid Green: it is 3.20:1 on Warm White,
	                                        UI and large text only. */

	/* --- Action --- */
	--action:      var(--vivid-green);
	--action-ink:  var(--ink);           /* Ink on Vivid Green — 5.22:1 AA, the approved pairing */

	/* --- Signal --- */
	/* Lime is how the brand points: intelligence at work. 1–3% of a
	   composition, dark ground only. Never a CTA, never a background or wash,
	   never type on Warm White (1.38:1), never a replacement for CINC green.

	   It is a SEMANTIC token, not a decorative one. Do not reach for it for
	   hover states, focus rings, borders, or any interaction chrome — a
	   footer's worth of links tinted Lime turns the signal into the site's
	   generic accent, and the deck is explicit that once it appears more than
	   a few times on a page it has stopped meaning anything. Use it where
	   something genuinely is intelligence, a recommendation, or the one line
	   that matters on a declaring surface. */
	--signal:      var(--lime);
	--signal-ink:  var(--ink);           /* Ink on Lime — 12.07:1 AAA */

	/* --- Focus --- */
	/* Evergreen, not Vivid Green. The deck calls Evergreen "accessible
	   functional green, light surfaces only" — a focus ring is exactly that
	   functional use, while Vivid Green is for recognition and action. It is
	   also twice the contrast: 6.32:1 against 3.20:1, where 3:1 is the floor
	   for non-text UI. Use --focus-inverse on declaring surfaces. */
	--focus:         var(--evergreen);
	--focus-inverse: var(--warm-white);

	/* --- Elevation --- */
	/* Ink at low alpha. Two levels: raised sits on the page, overlay sits
	   above other content. Kept here so cards across modules cannot drift
	   into three slightly different shadows. */
	--shadowRaised:  0 1px 2px rgba(23, 27, 25, .04),
	                 0 6px 24px rgba(23, 27, 25, .12);
	--shadowOverlay: 0 10px 34px rgba(23, 27, 25, .16);

	/* --- Lines --- */
	--line:            var(--stone);       /* Decorative dividers only */
	--line-functional: var(--stone-deep);  /* Borders and input outlines that carry meaning — 3.20:1 */

	/* --- Product semantic --- */
	/* Rank 6 in the deck's dominance order: functional meaning, deliberately
	   NOT drawn from the expressive secondaries — p34 is explicit that the two
	   sets are never interchangeable. The deck does not publish semantic values,
	   so these are chosen to clear AA on Warm White and read as functional
	   rather than brand. Replace if a product semantic palette is issued.
	     error   6.26:1   success 6.32:1   warning 5.68:1 */
	--error:   #B3261E;
	--success: var(--evergreen);
	--warning: #8A5A00;


	/* ============ TYPE ============ */

	--inter:    'Inter', system-ui, -apple-system, 'Segoe UI', Arial, sans-serif;
	--playfair: 'Playfair Display', Georgia, 'Times New Roman', serif;

	/* Inter Variable — the deck uses intermediate weights (450/550/650/750),
	   so the variable font must load, not the static cuts.

	   These are rungs on a scale, not roles: the role comments record where
	   each rung is currently spent. The deck quotes BANDS rather than single
	   values (400–450 body, 400–500 UI, 600–650 eyebrow, 650–700 hero,
	   650–750 data) and every role sits at the TOP of its band, because Inter
	   Variable renders lighter on screen than the static cut the comps were
	   set in. Moving a role down a rung is a visual decision, not a fix. */
	--light:    300;  /* Editorial display and quotations. A display voice, not a reading weight. */
	--regular:  400;  /* Floor of the body band. Nothing sits here now. */

	/* PLAYFAIR'S RULED WEIGHT — author ruling, 2026-08-26: no Playfair above
	   400, emphasis spans included, with ONE exception ruled the same day:
	   .heading2 sits at 450 (--book's rung), set in _typography.css. Every
	   other rule that weights Playfair points here. The comps measure
	   Playfair at 500 throughout; that fidelity note survives in the modules
	   that measured it. */
	--serif-weight: 400;
	--book:     450;  /* Body copy, leads, footer links — the reading weight */
	--medium:   500;  /* UI body, and the Playfair cut of H1/H2 */
	--display:  550;  /* Nav links, tab labels, card titles, form labels */
	--semibold: 600;  /* Floor of the eyebrow/button band. Nothing sits here now. */
	--bold:     700;  /* Hero display, data display, and the primary button —
	                     where 18.66px/700 is load-bearing, since WCAG counts
	                     that as large-scale text and drops the contrast bar
	                     from 4.5:1 to 3:1. Do not lighten it. */
	--data:     650;  /* Eyebrows, buttons, H2/H3, bare heading tags */
	--heavy:    750;  /* Data display ceiling. Never body copy. */


	/* ============ FORM ============ */
	/* Not specified in the brand deck — carried from the CINC system
	   and open to revision once composition specs land. */

	/* The page grid, measured off the comp: content sits 108 in at 1440, so
	   the column is 1224 wide.

	   A PERCENTAGE, not vw: it measures the containing block rather than the
	   viewport, so the scrollbar is out of it and the column is the same
	   width on every platform.

	   The 108 cap is not decoration. A percentage keeps growing with the
	   window while max-width holds the box still, so past 1440 an uncapped
	   gutter would eat the column it is supposed to define — 7.5% of 1920 is
	   144 a side, and the content would come out 1152 instead of 1224. The
	   cap is what stops it at the width where the container maxes out.

	   It only means "the page gutter" on a full-width parent. A module
	   working inside a narrower box states its own percentage of THAT box
	   rather than subtracting from this one — see magic-moment, which does
	   and still lands its copy on the grid at every width. */
	--pageGutter: clamp(16px, 7.5%, 108px);

	/* The inset for a slab that breaks out of the page column — the magic
	   moment band and the CTA card. 30 of 1440.

	   The comp draws these at different widths: the band runs x30 to x1410
	   and the card x39 to x1400, 9.5 apart. Two near-identical dark slabs
	   that close apart reads as drift rather than intent, the same way the
	   icon band's hand placement did, so they share one value here. 30 is
	   the band's, chosen because its edge lands on a whole pixel with no
	   antialiasing at all — the card's does not. */
	--bandInset: clamp(8px, 2.0833%, 30px);

	/* The third inset, and the one the comps use most: 72 of 1440, which is
	   5%. It is the HEADER PILL's margin, and that is not a coincidence —
	   every box that sits on it is aligning with the nav: the testimonial
	   track, the Connect cards, the partner slab, the statement cards.

	   So the page has three insets and each says something different:

	     --pageGutter  108   text. The reading column, 1224 wide.
	     --pillGutter   72   boxes. Cards and slabs, aligned with the nav pill.
	     --bandInset    30   near-bleed bands that only just clear the edge.

	   Named for the pill because that is the thing on screen a reader can
	   check it against, and because it answers "why 72?" without anyone
	   having to go and measure. Five modules had written this clamp out
	   locally before it had a name. */
	--pillGutter: clamp(16px, 5%, 72px);

	/* One box radius. Measured at 4x on every rounded box on comp page 1 —
	   the magic band, the glass panel, the white UI card, the tabs bar, the
	   active tab pill and the CTA card are all 12. There used to be a --rad
	   of 16 and a --radInner of 12 and modules picked whichever suited, which
	   is what made the corners look unrelated to each other.

	   A box inside another box is CONCENTRIC: its radius is the outer radius
	   less the gap between them, floored at 0.

	       border-radius: max(0px, calc(var(--rad) - <the inset>));

	   Concentric corners stay parallel all the way round. Two boxes both set
	   to 12 with padding between them do not — the inner curve is tighter
	   relative to its own size and the gap pinches at the corners, which is
	   what makes a nest of boxes look unrelated.

	   Where the inset is larger than the radius the inner box is square, and
	   that is correct rather than a bug: at that distance there is no curve
	   left to stay parallel to.

	   --radPill is a real pill — 999 rather than a number that only happens
	   to round a 76px bar and leaves a 46px button slightly square. */
	--rad:      12px;
	--radPill:  999px;

	/* The small icon tile — the 60 square Stone tile in promo-cards and the
	   partner panel, and the 44 avatar in the AI band. Half the box radius,
	   so it stays in step if --rad ever moves.

	   The comps measure this tile at 6 in promo-cards and 10 in the partner
	   panel — both read off the corner curves, both exact, on what is
	   otherwise the same 60 square Stone tile. One of the two is drift, and
	   half the box radius is the value that belongs to a system rather than
	   to whichever artboard was measured last. The concentric rule does not
	   apply: these tiles sit IN a layout, not nested in a rounded corner. */
	--radTile:  calc(var(--rad) / 2);

	/* Focus rings on small inline things, where a box radius reads as a blob. */
	--radFocus: 2px;

	/* Vertical rhythm scale. Drives the Layout group's spacing dropdowns, so
	   authors pick a step on the scale rather than typing arbitrary pixels. */
	--space-xs:  clamp(16px, 2vw, 24px);
	--space-s:   clamp(24px, 3vw, 40px);
	--space-m:   clamp(40px, 5vw, 64px);
	--space-l:   clamp(64px, 7vw, 96px);
	--space-xl:  clamp(96px, 10vw, 144px);
	/* A rung above Extra Large, added because the homepage bands wanted more
	   air than xl gives them. Derived, not picked: each step multiplies the
	   one below by about 1.5 at both ends (64→96→144 on the floor,
	   96→144→216 on the ceiling), and the vw term is set so it MEETS the
	   ceiling at 1440 — the page measure — exactly as the other rungs do
	   (10vw × 1440 = 144, 15vw × 1440 = 216). */
	--space-2xl: clamp(144px, 15vw, 216px);

	--ease:    cubic-bezier(0.16, 1, 0.3, 1);
	--easeOut: cubic-bezier(0, 0, 0.37, 1);

	/* The one curve in the theme that is NOT front-loaded, and the reason it
	   has to exist.

	   --ease is an expo-out: it delivers most of its value almost immediately
	   and then creeps to the end. That is right for a thing whose whole travel
	   is visible — a card lifting two pixels, an arrow crossing its window —
	   because the eye reads the arrival and the tail just settles it.

	   It is wrong for the button's spotlight, because that fill OVERSHOOTS on
	   purpose. The circle scales to 1.9 so it still reaches the far corner
	   when the pointer entered at the near one, but a "Book a Demo" pill is
	   covered at scale 1.15 — 60.5% of the way. Solved against the curves:

	     --ease      covered at  94ms of 700
	     --easeOut   covered at 238ms of 700
	     --easeBloom covered at 376ms of 700

	   So on --ease the fill was finished in a tenth of the duration and the
	   remaining 606ms was invisible growth past the edges. Leaving looked
	   slower than arriving because on the way out that same overshoot IS
	   visible: the circle shrinks back through the range it grew through
	   unseen. Same duration, same curve, and the two directions read as
	   completely different speeds — which is exactly what was reported.

	   easeInOutCubic spends its time where the eye can see it. Nothing else in
	   the theme should reach for this unless it also animates a value past the
	   point where the change stops being visible. */
	--easeBloom: cubic-bezier(0.65, 0, 0.35, 1);

	/* Staggered reveals, in two tiers.

	   PAGE — sections arriving on scroll. The motion is part of the reading
	   experience, so it can take its time.
	   INTERFACE — menus and compact lists, where someone has already asked
	   for the content and is waiting on it. Same easing and direction so it
	   reads as the same family, at a speed that does not make anyone wait. */

	--cascadeDuration:   1s;
	--cascadeStep:       0.12s;
	--cascadeShift:      20px;

	--cascadeDurationUI: 0.4s;
	--cascadeStepUI:     0.05s;
	--cascadeShiftUI:    10px;

	/* --- Interaction motion ---------------------------------------------

	   Three durations, and the split between them is the point. A pointer
	   interaction is a CONVERSATION: the surface answers instantly, commits
	   slowly, and lets go faster than it was pressed.

	     quick   colour, opacity, border — state that should read as already
	             true by the time the eye arrives. Matches the 0.15s the base
	             button rule has always used, rounded down.
	     hover   transform on hover — the one place motion is allowed to be
	             seen. 0.24 is under the 0.25 the button shape already sets,
	             so nothing that inherits both fights.
	     press   the press DOWN only. Deliberately a third of the hover: a
	             button that sinks as slowly as it lifts feels broken, because
	             a real one bottoms out the moment it is touched. The release
	             is not given a token — it uses --durHover, which is what
	             makes down-fast/up-slow the default rather than a per-module
	             decision.

	   The three distances are the smallest that survive a 2x screen without
	   reading as a rendering artefact. They are travel, not decoration: at
	   -2px a card is lifting, at -8px it is jumping, and the deck's register
	   is confident rather than eager.

	   --nudge is 3px because solution-cards measured it there first, and one
	   arrow travelling 3 while another travels 6 is the kind of drift the
	   radius tokens were introduced to stop.

	   --zoom is a SCALE, so its perceived travel depends on the frame: 1.04
	   on a 620-wide media frame moves the far edge 12px, which is the same
	   order as the card lift. On a much larger frame it will read as more —
	   set a smaller value locally there rather than lowering this one. */
	--durQuick: .12s;
	--durHover: .24s;
	--durPress: .08s;

	--lift:  -2px;   /* cards and panels rising under the pointer */
	--nudge:  3px;   /* an arrow or chevron travelling toward its destination */
	--zoom:   1.04;  /* an image growing inside a frame that clips it.
	                    Photographs and footage only — never a screenshot, a
	                    mockup, a chart or a logo. See _motion.css section 4. */

	/* Two longer durations, for the two things a button does that are not a
	   state change but a small piece of choreography.

	   --durThrow is the arrow's round trip. It is longer than --durHover
	   because the travel is 320% of the window rather than two pixels: at
	   0.24 the arrows read as blinking from one side to the other, and the
	   handover — the moment one is leaving and the other arriving — never
	   lands on screen.

	   --durBloom is the fill arriving from under the pointer. Longest of the
	   set by some way, and deliberately so: it is the only motion here that
	   crosses the whole control, and a fill that crosses 180px in the time a
	   colour crosses nothing reads as a flash rather than as a sweep.

	   It was .52 and that was still too quick — the bloom arrived before the
	   eye had followed it and the effect read as the button changing colour
	   after all, which is the thing it exists to not be. At .7 the circle is
	   legible as a circle: you can see where it started, which is the whole
	   point of it starting where the pointer did.

	   It also drives the form submit's linear sweep, so the two stay in step
	   even though they travel differently. Raising it further starts to feel
	   like lag on the press, which answers in .08 — keep roughly an order of
	   magnitude between them. */
	   --durBloomOut is the fill LEAVING, and it is deliberately less than half
	   the arrival. Arriving is the event; leaving is clearing up after it, and
	   a fill that takes as long to go as it took to come reads as the button
	   being slow to let go. It keeps --easeOut rather than --easeBloom,
	   because on the way out the overshoot is uncovered first and there is
	   nothing to spread the time over. */
	--durThrow: .36s;
	--durBloom: .7s;
	--durBloomOut: .3s;


	/* ============ 3. COMPAT ============ */
	/* Legacy CINC names mapped onto Vivid roles so modules ported from
	   CINC SystemJune-2024 render sensibly during the migration.
	   These are approximations, not equivalents — the palettes differ.
	   Delete this block once nothing references it. */

	--cincGreen:     var(--vivid-green);
	--electricGreen: var(--lime);      /* NOTE: Lime is NOT a drop-in for electric green.
	                                      Any ported module using this as a CTA or on a
	                                      light surface must be reworked, not remapped. */
	--forestGreen:   var(--evergreen);
	--blackGreen:    var(--deep-green);

	--black:   var(--ink);
	--gray900: var(--slate);
	--gray700: var(--slate);
	--gray500: var(--stone-deep);
	--gray400: var(--stone-deep);
	--gray300: var(--stone);
	--gray200: var(--stone);
	--gray100: var(--cloud);
	--white:   var(--warm-white);
}


/* ============ 2. THE FOUR BOXES THEMSELVES ============
   The scope in section 3 reaches DESCENDANTS of the chrome. These are the
   things the chrome roots need on their own account. */
.sticky-nav,
.site-header,
.site-footer,
.skip-link {
	box-sizing: border-box;

	/* THE BODY RULE, RESTATED — elements/_typography.css sets these five on
	   <body>, and <body> belongs to the host page. Section 3 includes that
	   file scoped, which covers p, a, h1-h6 and the rest, but a scoped copy
	   can never reach `body`: nested, the selector becomes `.site-header body`
	   and matches nothing.

	   So without this the chrome inherits the 2024 theme's <body> instead.
	   Measured on the collision harness, every one of 227 elements in the
	   header and footer came back wrong: Montserrat for Inter, 10px for
	   17.28px, weight 400 for 450, 15px leading for 27.648px, and #2E3641
	   for Ink. The border colours went with the text, since they resolve
	   through currentColor.

	   font-size is the one that would have been hardest to spot later. The
	   2024 gdco theme sets `body { font-size: 1rem }` against a 10px root, so
	   the chrome does not render at some obviously-broken size — it renders
	   at a plausible, quietly-too-small 10px. */
	font-family: var(--inter);
	font-size: clamp(1.7rem, 1.2vw, 1.8rem);
	font-weight: var(--book);
	line-height: 1.6;
	color: var(--ink-primary);
}

/* .surface-declaring — Deep Green ground, Warm White type, Lime for the one
   accent line. The footer carries this class ON ITSELF, so it is out of reach
   of a descendant scope and has to be stated here.

   HAND-STATED, one of the two exceptions named at the top. objects/_containers.css
   cannot be included: it opens with a :root block, and a :root nested inside a
   class is invalid CSS — the browser drops it and everything after it in that
   block. If the surface roles are ever revised there, revise them here too.
   Copied verbatim from _containers.css as of 2026-08-26. */
:is(.site-header, .site-footer).surface-declaring {
	background-color: var(--surface-declaring);
	color: var(--ink-inverse);

	/* Lime is legible only on dark ground, and only in small quantity. */
	.eyebrow, .overline { color: var(--signal); }
}


/* ============ 3. EVERYTHING INSIDE THE CHROME ============ */
:is(.sticky-nav, .site-header, .site-footer) {

	/* box-sizing for descendants, and the button normalize. The chrome has
	   three <button>s in it — the two dropdown triggers, the menu toggle and
	   the footer's Manage Cookies control — and every one of them depends on
	   `background: transparent; border: none; font: inherit` being true before
	   module.css starts. On a 2024 page the alternative is that theme's button
	   styling, which is not nothing. */
	*, *:before, *:after {
  box-sizing: border-box;
}

button {
  border: none;
  margin: 0;
  padding: 0;
  width: auto;
  overflow: visible;

  background: transparent;

  /* inherit font & color from ancestor */
  color: inherit;
  font: inherit;

  /* Normalize `line-height`. Cannot be changed from `normal` in Firefox 4+. */
  line-height: normal;

  /* Corrects font smoothing for webkit */
  -webkit-font-smoothing: inherit;
  -moz-osx-font-smoothing: inherit;

  /* Corrects inability to style clickable `input` types in iOS */
  -webkit-appearance: none;
}

	/* NORMALIZE COMES WITH THE RESET, AND IT IS NOT OPTIONAL — this pair is
	   the one place a "just take what the chrome needs" file goes wrong, and
	   the A/B render caught it.

	   The two disagree on purpose, and main.css settles it by order:

	       generic/_reset.css      button { line-height: normal }
	       generic/_normalize.css  button, input, … { line-height: 1.15 }

	   Both are (0,0,1); normalize is included second, so 1.15 wins and every
	   button on a Vivid page computes 19.55px at 17px. Ship the reset here
	   without the normalize and the scoped copy is (0,2,0) — it out-specifies
	   the normalize rule that is still winning everywhere else, and the menu
	   toggle alone starts computing `normal`.

	   Nothing moved by a pixel when it did: the toggle is a fixed 46×46 box
	   holding two explicitly-sized bars and a clipped label, so the A/B page
	   heights and every bounding box matched exactly. It is still the wrong
	   outcome. A file whose whole claim is "the same rules, one class heavier"
	   cannot quietly resolve a tie the theme resolves the other way. */
	/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */

/* Document
   ========================================================================== */

/**
 * 1. Correct the line height in all browsers.
 * 2. Prevent adjustments of font size after orientation changes in iOS.
 */

* { box-sizing: border-box; }
html {
  line-height: 1.15; /* 1 */
  -webkit-text-size-adjust: 100%; /* 2 */
  scroll-behavior: smooth;
}

/* Sections
   ========================================================================== */

/**
 * Remove the margin in all browsers.
 */
 

body {
  margin: 0;
	font-size: 10px;
}

/**
 * Correct the font size and margin on `h1` elements within `section` and
 * `article` contexts in Chrome, Firefox, and Safari.
 */

h1 {
  font-size: 2em;
  margin: 0.67em 0;
}

/* Grouping content
   ========================================================================== */

/**
 * Add the correct box sizing in Firefox.
 */

hr {
  box-sizing: content-box;
  height: 0;
}

/**
 * 1. Correct the inheritance and scaling of font size in all browsers.
 * 2. Correct the odd `em` font sizing in all browsers.
 */

pre {
  font-family: monospace, monospace; /* 1 */
  font-size: 1em; /* 2 */
}

/* Text-level semantics
   ========================================================================== */

/**
 * 1. Remove the bottom border in Chrome 57-
 * 2. Add the correct text decoration in Chrome, Edge, Opera, and Safari.
 */

abbr[title] {
  border-bottom: none; /* 1 */
  text-decoration: underline; /* 2 */
  text-decoration: underline dotted; /* 2 */
}

/**
 * Add the correct font weight in Chrome, Edge, and Safari.
 */

b,
strong {
  font-weight: bolder;
}

/**
 * 1. Correct the inheritance and scaling of font size in all browsers.
 * 2. Correct the odd `em` font sizing in all browsers.
 */

code,
kbd,
samp {
  font-family: monospace, monospace; /* 1 */
  font-size: 1em; /* 2 */
}

/**
 * Add the correct font size in all browsers.
 */

small {
  font-size: 80%;
}

/**
 * Prevent `sub` and `sup` elements from affecting the line height in
 * all browsers.
 */

sub,
sup {
  font-size: 75%;
  line-height: 0;
  position: relative;
  vertical-align: baseline;
}

sub {
  bottom: -0.25em;
}

sup {
  top: -0.5em;
}

/* Forms
   ========================================================================== */

/**
 * 1. Change the font styles in all browsers.
 * 2. Remove the margin in Firefox and Safari.
 */

button,
input,
optgroup,
select,
textarea {
  font-family: inherit; /* 1 */
  font-size: 100%; /* 1 */
  line-height: 1.15; /* 1 */
  margin: 0; /* 2 */
}

/**
 * Remove the inheritance of text transform in Edge and Firefox.
 * 1. Remove the inheritance of text transform in Firefox.
 */

button,
select { /* 1 */
  text-transform: none;
}

/**
 * Correct the inability to style clickable types in iOS and Safari.
 */

button,
[type="button"],
[type="reset"],
[type="submit"] {
  -webkit-appearance: button;
}

/**
 * Remove the inner border and padding in Firefox.
 */

button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
  border-style: none;
  padding: 0;
}

/**
 * Restore the focus styles unset by the previous rule.
 */

button:-moz-focusring,
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring {
  outline: 1px dotted ButtonText;
}

/**
 * Correct the padding in Firefox.
 */

fieldset {
  padding: 0.35em 0.75em 0.625em;
}

/**
 * Remove the padding so developers are not caught out when they zero out `fieldset` elements in all browsers.
 */

legend {
  padding: 0;
}

/**
 * Add the correct vertical alignment in Chrome, Firefox, and Opera.
 */

progress {
  vertical-align: baseline;
}

/**
 * Correct the cursor style of increment and decrement buttons in Chrome.
 */

[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
  height: auto;
}

/**
 * 1. Correct the odd appearance in Chrome and Safari.
 * 2. Correct the outline style in Safari.
 */

[type="search"] {
  -webkit-appearance: textfield; /* 1 */
  outline-offset: -2px; /* 2 */
}

/**
 * Remove the inner padding in Chrome and Safari on macOS.
 */

[type="search"]::-webkit-search-decoration {
  -webkit-appearance: none;
}

/**
 * 1. Correct the inability to style clickable types in iOS and Safari.
 * 2. Change font properties to `inherit` in Safari.
 */

::-webkit-file-upload-button {
  -webkit-appearance: button; /* 1 */
  font: inherit; /* 2 */
}

/* Interactive
   ========================================================================== */

/*
 * Add the correct display in Edge and Firefox.
 */

details {
  display: block;
}

/*
 * Add the correct display in all browsers.
 */

summary {
  display: list-item;
}

	/* .container — the footer's only structural borrowing.

	   HAND-STATED, the second of the two exceptions, and the same reason:
	   _containers.css opens with :root, and also styles `main` and `section`
	   at element level, neither of which has any business on a 2024 page.

	   This is the base rule only. footer module.css already sets its own
	   --containerWidth and swaps --pageGutter for --pillGutter, so the two
	   declarations below plus the fallback are the whole of what it is
	   missing. The .container:has(> .card) inset and the -wide/-desktop/-narrow
	   presets are not reachable from the chrome and are left out.

	   Copied from _containers.css as of 2026-08-26. */
	.container {
		--containerWidth: 1440px;

		max-width: var(--containerWidth);
		margin: 0 auto;
		padding-inline: var(--pageGutter);
	}

	/* THE HOST'S ELEMENT STYLING, HANDED BACK TO INHERITANCE.

	   Section 2 puts the family, size, weight, leading and colour on the
	   chrome's roots, and everything inside is supposed to inherit them. That
	   works right up until the host page styles a bare tag, because an
	   explicit declaration beats an inherited value at any specificity — even
	   (0,0,1) against a root that said otherwise.

	   The 2024 theme does exactly that:

	       .h1, h1     { font-family: Montserrat; font-size: 64px; color: #2e3341 }
	       .h4,.h5,h4,h5 { font-family: Montserrat; font-weight: 800 }
	       .h6, h6     { font-family: Montserrat; font-size: 1rem; color: #2e3641 }

	   Measured on the harness before this rule existed: the footer's four
	   <h2 class="column-heading"> elements were the last five things on the
	   page still rendering in Montserrat, while every element around them had
	   come right.

	   :where() ON PURPOSE — it contributes nothing, so this whole block sits
	   at the scope's own (0,1,0). That is the exact rung it needs: above the
	   host's bare-element rules at (0,0,1), and below the theme's own scoped
	   element rules at (0,2,0), which are included immediately after and must
	   still win. Written with :is() it would out-rank the type scale it is
	   here to protect.

	   Only inherited properties, and only on elements that carry text. <i> is
	   deliberately absent: the icons are Font Awesome, and handing their
	   family back to inheritance would replace every glyph with a letter. */
	:where(h1, h2, h3, h4, h5, h6, p, a, ul, ol, li, span, div, nav,
	       address, strong, em, b, small, button) {
		font-family: inherit;
		font-size: inherit;
		line-height: inherit;
		letter-spacing: inherit;
		text-transform: inherit;
		color: inherit;
	}

	/* Type. The footer is real prose — four columns of links under <h2>
	   headings, a rich-text address, a legal row — and every one of those tags
	   is styled at element level here rather than by module.css. Without it
	   the footer's headings and links take the 2024 theme's h2 and a, which
	   are a different family, scale and colour.

	   The html/body rules at the head of the file are the part a scope cannot
	   carry: nested, `html, body` becomes `.site-header html` and matches
	   nothing. Section 2 restates those five declarations on the chrome roots,
	   which is where they take effect from. Everything else in the file — p,
	   a, h1-h6, ul, ol, blockquote, .eyebrow, .overline, .rich-heading — lands
	   normally. */
	/* Type scale from the Vivid deck (p37–40).
   Sizes flex between the spec's min and max via clamp(); leading and tracking
   follow the ranges given. Tracking tightens as size grows: 0 at body sizes,
   −0.02em at display, −0.03em at hero. */

/* 1rem = 10px, as the GDCO system sets it. Every size in this theme is
   expressed in rem against that root, so a value reads as its px measurement
   divided by ten and still answers the reader's browser text setting.
   The reading size then rides on top, in rem — a <body> left at 10px would
   drop every inherited text node to 10px. */
html, body {
	font-size: 10px;
}

html {
	line-height: 1.15;
	-webkit-text-size-adjust: 100%;
	-webkit-font-smoothing: antialiased;
	-moz-osx-font-smoothing: grayscale;
}

body {
	font-family: var(--inter);
	font-size: clamp(1.7rem, 1.2vw, 1.8rem);
	font-weight: var(--book);
	line-height: 1.6;
	overflow-wrap: break-word;
	background-color: var(--surface);
	color: var(--ink-primary);
	-webkit-font-smoothing: antialiased;
	-moz-osx-font-smoothing: grayscale;
}

/* Handles word breaking for a few specific languages which handle breaks in words
differently. If your content is not translated into these languages, you can safely
remove this. */

html[lang^="ja"] body,
html[lang^="zh"] body,
html[lang^="ko"] body {
	line-break: strict;
	overflow-wrap: normal;
	word-break: break-all;
}

/* Paragraphs — Body: 17–18px / 156–165% / 0 tracking / 450 */

p {
	font-size: clamp(1.7rem, 1.2vw, 1.8rem);
	line-height: 1.6;
	margin-top: 0;
	margin-bottom: 1em;
}

/* Anchors */

a {
	cursor: pointer;
	text-decoration: none;
	color: inherit;
}

/* Headings */

h1, h2, h3, h4, h5, h6 {
	margin: 0 0 1.4rem;
	font-weight: var(--data);
	/* Even line lengths on every headline that wraps. On the element, not the
	   heading classes, so a module-local heading (the tabs panel line, a card
	   heading) gets it by being a real heading — which they all are now. The
	   engine caps balancing at a handful of lines, so running copy is never
	   affected, and a <br> in a rich heading is respected as a hard break. */
	text-wrap: balance;
}

/* Any element carrying a heading class gets the same box, so the tag can be
   chosen for document structure without changing how the heading looks.
   Modules set the visual size with the class and the level with the field. */
.hero-display,
.heading1,
.heading2,
.heading3 {
	margin: 0 0 .5em;
	font-family: var(--inter);
}

/* Hero display — 64–80px / 102–106% / −0.03em / 650–700 */
.hero-display {
	font-size: clamp(4rem, 6vw, 8rem);
	line-height: 1.04;
	font-weight: var(--bold);
	letter-spacing: -.03em;
	text-wrap: balance;
}

/* Page H1. Playfair in every comp — see delta 01 in the build plan; if Brand
   rules for Inter this reverts here and nowhere else.

   61.92px is derived, not read: the display headings are outlined to paths
   in the SVG export, so there is no size to extract.

   Derive display sizes from CAP HEIGHT, not line width. An earlier pass
   solved the comp's line widths against rendered Playfair and got 59.2 —
   wrong, because Illustrator sets text about 5% narrower than the browser,
   so matching width forces the size down. A vertical measure is immune to
   that: the comp's cap-R is 44.62px, and Playfair's measured cap ratio is
   0.725 (not the 0.700 its metrics imply), giving 61.54. Independent
   shaping with kerning gave 61.96.

   The width method failed for a simpler reason than "Illustrator renders
   narrower": the comp tracks display type at -0.03em and the first pass
   applied -0.01em, so the text ran wide and the size was shrunk to
   compensate. Confirmed from the source file — the Character panel shows
   Illustrator tracking of -40, which is -0.04em on Inter display headings.

   At 61.92 with -0.03em the lines measure 888.9 and 1003.9 against the
   comp's 887 and 999.5, and the cap height agrees. Size and tracking are
   one measurement; solving either alone gives a wrong answer. */
.heading1 {
	font-family: var(--playfair);
	font-size: clamp(3.4rem, 4.3vw, 6.192rem);
	line-height: 1;
	/* --serif-weight, the theme-wide Playfair cap. The comps measure 500;
	   the author ruled all Playfair down to 400. */
	font-weight: var(--serif-weight);
	letter-spacing: -.03em;
	text-wrap: balance;

	/* The Inter cut, for the headings the comps set in sans */
	&.sans {
		font-family: var(--inter);
		font-size: clamp(3.4rem, 5vw, 6.4rem);
		line-height: 1.07;
		/* 650, not the band top. Measured: the magic-moment band's two lines
		   ink 357 and 481 wide over a 60px cap-to-descender, which resolves
		   to 64 / 650 / -0.02em within 2px on all three. 700 runs it 1.8%
		   wide. This is the one display size with a direct measurement. */
		font-weight: var(--data);
		letter-spacing: -.02em;
	}
}

/* Section H2. Playfair, as the comps set it. */
.heading2 {
	font-family: var(--playfair);
	font-size: clamp(3rem, 3.6vw, 5.16rem);
	line-height: 1.05;
	/* 450 — the one ruled exception to the Playfair cap: H1 display sits at
	   --serif-weight (400), the section H2 a half-step up on --book's rung.
	   See the --serif-weight note in _variables.css. */
	font-weight: var(--book);
	letter-spacing: -.03em;
	text-wrap: balance;

	&.sans {
		font-family: var(--inter);
		font-size: clamp(3rem, 4vw, 4.8rem);
		line-height: 1.12;
		font-weight: var(--data);
		letter-spacing: -.02em;
	}
}

/* H3 — 28–32px / 118–121% / −0.015em / 650.
   The deck quotes 600. Every role in this file sits at the top of its band
   because Inter Variable renders lighter on screen than the static cut the
   comps were set in; H3 has no band, so this one is a judgement call. */
.heading3 {
	font-size: clamp(2.4rem, 2.6vw, 3.2rem);
	line-height: 1.19;
	font-weight: var(--data);
	letter-spacing: -.015em;
}

/* Lead paragraph — 20–22px / 150–155% / 0 / 400–450 */
.lead {
	font-size: clamp(1.9rem, 1.6vw, 2.2rem);
	line-height: 1.52;
	font-weight: var(--book);
	letter-spacing: 0;
	text-wrap: pretty;
}

/* UI body — 14–16px / 143–150% / 0 / 400–500. Never below 14px. */
.ui-body {
	font-size: clamp(1.4rem, 1.1vw, 1.6rem);
	line-height: 1.47;
	font-weight: var(--medium);
	letter-spacing: 0;
}

/* Data display — 44–72px / 100–106% / −0.02em / 650–750 */
.data-display {
	font-size: clamp(4rem, 5vw, 7.2rem);
	line-height: 1;
	font-weight: var(--bold);
	letter-spacing: -.02em;
	font-variant-numeric: tabular-nums;
}

/* Editorial display — Inter 300 at 48px, or Playfair for selective contrast.
   Playfair is editorial expression only; product and UI stay Inter. */
.editorial {
	font-size: clamp(3rem, 3.6vw, 4.8rem);
	line-height: 1.21;
	font-weight: var(--light);
	letter-spacing: -.02em;
	text-wrap: balance;

	&.serif {
		font-family: var(--playfair);
		font-weight: var(--serif-weight);
	}
}

/* Rich headings.
   Headlines that carry two type treatments use a bold-only richtext field: the
   author bolds the phrase that changes, and the module decides what "bold"
   means through two custom properties. One field, no second heading input, and
   the emphasis travels with the words if the copy is rewritten.

   Pair with a real heading tag — strip the richtext <p> wrapper in HubL so the
   markup stays valid and semantic:

     
     <h2 class="heading2 rich-heading">null</h2>

   The old CINC system put these in <div class="heading"> instead, which meant
   the page had no real headings for search or screen readers. */

.rich-heading {
	--emphasis-weight: inherit;
	--emphasis-color: inherit;

	strong, b {
		font-weight: var(--emphasis-weight);
		color: var(--emphasis-color);
	}

	/* Belt and braces for any wrapper that survives — inherit rather than
	   letting a nested <p> or <span> reset the heading's own setting. */
	> p, > span, > div {
		font-size: inherit;
		font-weight: inherit;
		color: inherit;
		line-height: inherit;
		letter-spacing: inherit;
		margin: 0;
	}
}

/* Eyebrow — one theme-wide form: 15px Inter Bold in ALL CAPS at .18em, at
   every viewport. The weight is section-header's measured 700; the size, the
   caps and the loose tracking are ruled departures from the comps
   (2026-08-26), which drew section eyebrows title case at 17.28 with
   tracking 0 — the size came down two points with the caps, and .18em sits
   inside the deck's published +0.14 to +0.22 band for tracked caps. One
   size, no tablet step: 15px was already the old sub-768 step, so the
   media query collapsed into the base value.

   Block, not inline-block: an inline-block sits on a line box governed by the
   container's 1.6 strut, which quietly adds ~4px under it and puts measured
   eyebrow → heading gaps out. Margin is zeroed here so each module states
   only the gap it measured.

   Colour is deliberately absent: .surface-announcing and .surface-declaring
   (_containers.css) and the modules own the approved AA pairings, and a
   colour here would fight them.

   `.overline` is the CINC-system name for the same thing; both are supported
   so ported modules keep working. */
.eyebrow,
.overline {
	display: block;
	font-size: 1.5rem;
	line-height: 1.15;
	font-weight: var(--bold);
	letter-spacing: .18em;
	text-transform: uppercase;
	margin: 0;
	text-wrap: initial;

	&.pill {
		border: 1px solid currentColor;
		border-radius: var(--radPill);
		padding: .8em 2em;
		font-weight: var(--display);
	}
}

/* Lists */

ul, ol {
	margin: 0 0 1.4rem;
}

ul ul, ol ul, ul ol, ol ol {
	margin: 0;
}

ul.no-list {
	list-style: none;
	margin: 0;
	padding-left: 0;
}

/* Code blocks */

pre { overflow: auto; }
code { vertical-align: bottom; }

/* Blockquotes */

blockquote {
	border-left: 2px solid var(--line-functional);
	margin: 0 0 1.4rem;
	padding-left: 0.7rem;
}

/* Horizontal rules */

hr {
	border: none;
	border-bottom: 1px solid var(--line);
}

/* Image alt text */

img {
	width: 100%;
	max-width: 100%;
	vertical-align: middle;
	border: none;
}

	/* The button: shape, the three variants, the disabled state, and the form
	   submit rules that come along with them and match nothing in here. */
	a, button {
	text-underline-offset: 5px;
}

button,
.button,
.hs-button {
	cursor: pointer;
	display: inline-block;
	text-align: center;
	transition: all 0.15s linear;
	white-space: normal;
}

button:disabled,
.button:disabled,
.hs-button:disabled {
	background-color: var(--stone);
	border-color: var(--stone);
	color: var(--stone-deep);
}

/* No button */

.no-button,
.no-button:hover,
.no-button:focus,
.no-button:active {
	background: none;
	border: none;
	border-radius: 0;
	color: initial;
	font-family: inherit;
	font-size: inherit;
	font-style: inherit;
	font-weight: inherit;
	letter-spacing: inherit;
	line-height: inherit;
	margin-bottom: 0;
	padding: 0;
	text-align: left;
	text-decoration: none;
	transition: none;
}

/* Shared shape for .btn and the HubSpot form submit, which cannot take our class.

   THIS BLOCK USED TO BE THE EYEBROW SETTING — 650 weight, .14em tracking,
   uppercase, 13-14px — and .primary overrode all four properties to the comps'
   label. The effect was two button treatments on one page: a primary reading
   as an 18.66px sentence-case label beside a secondary reading as a 13px
   tracked-caps micro button, 46 tall against 48.

   That was drift, not intent. Nothing in the deck or the comps asks for two
   button voices, no module overrode it, and no comment ever justified it — it
   was simply that .primary was fixed and its siblings were not. The same bug
   had already been found and fixed once for the form submit, which claimed the
   primary treatment through HubSpot's class list without taking it; that fix
   went to the submit and stopped there.

   So the label treatment lives HERE now, and every variant gets it. .primary,
   .secondary, .on-dark and the submit differ in colour and in nothing else.

   THE SIZE AND THE COLOUR ARE STILL ONE DECISION ON THE PRIMARY. White on
   Vivid Green is 3.34:1, which clears the 3:1 bar only because 18.66px at 700
   counts as large-scale text; as normal text it fails 4.5:1. Moving this size
   down moves that button out of compliance. The other variants are safe at any
   size — Evergreen on Warm White 6.32:1, Ink on white 15.87:1, white on Deep
   Green 14.94:1 — so 18.66 is a brand decision for them and a legal
   requirement for the primary. Do not lower it.

   inline-flex rather than inline-block, with a gap: every variant can now
   carry the two-arrow window macros/button.html emits for the throw hover, and
   a 1em box next to a text node needs a flex context to sit on the optical
   centre rather than the baseline.

   No transition here any more. css/utilities/_motion.css owns every animated
   property on a button and states them as an explicit list; a shorthand here
   would be silently replaced by it, which is worse than being absent. */


.btn {
	
	min-height: 46px;
	padding: 0 16px;
	border-radius: var(--radPill);
	font-family: var(--inter);
	font-size: 1.866rem;
	font-weight: var(--bold);
	letter-spacing: 0;
	text-transform: none;
	line-height: 1.25;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	gap: 8px;
	text-align: center;
	box-sizing: border-box;
	border: 1px solid transparent;


	/* NOTHING IN THE THEME USES THESE. Grepped across modules/ and templates/
	   and there is not one .btn.small or .btn.smallest. They are left in place
	   rather than deleted — removing markup hooks is not this change's job —
	   but they are rebased, because as written against the old tracked-caps
	   shape they would now render a 13px sentence-case label in a 46-tall pill
	   with the primary's padding, which is not a small button, it is a broken
	   one. Expressed as a reduction of the shape above so they stay in step
	   with it. Delete them if they are still unused when the build is done. */
	&.small {
		font-size: 1.6rem;
		min-height: 40px;
	}

	&.smallest {
		font-size: 1.4rem;
		min-height: 34px;
		padding: 0 14px;
	}

	/* The arrow, on every variant rather than only the primary. It used to sit
	   inside .primary, which was fine while that was the only variant with an
	   icon; macros/button.html now gives any of them the two-arrow window, and
	   an arrow that is .82em on one button and 1em on the next is the same
	   class of drift this file has just finished removing. */
	i {
		font-size: .82em;
		line-height: 1;
	}

	/* PRIMARY — Vivid Green. Intelligence Lime is NEVER the default CTA colour
	   (deck p33).

	   COLOUR ONLY. The shape, the size, the weight and the case all come from
	   btn_shape now — see the note there for why they moved, and for why the
	   18.66 is load-bearing on this variant specifically: white on Vivid Green
	   is 3.34:1 and legal only as large-scale text. Pure white rather than Warm
	   White buys 0.14 of that, which is worth taking and nowhere near enough on
	   its own; 4.5:1 is out of reach on this green at any white. The size and
	   the colour are one decision and changing either alone breaks it.

	   The pill is 46 tall wherever it appears — measured on every Vivid Green
	   button on comp page 1: header, magic-moment band, and the two further
	   down, all 46. That measurement is now in btn_shape, so it is 46 on every
	   variant rather than 46 on this one and 48 on its siblings. */
	&.primary {
		background-color: var(--action);
		color: var(--ink-inverse);

		&:hover {
			background-color: color-mix(in srgb, var(--action) 88%, var(--ink));
		}
	}

	/* SECONDARY — outlined, for light surfaces. Evergreen is the accessible
	   green for type on light ground. */
	&.secondary {
		background-color: transparent;
		color: var(--ink-accent);
		border-color: var(--line-functional);

		&:hover {
			border-color: var(--ink-accent);
			background-color: var(--community-mist);
		}
	}

	/* ON DARK — for declaring (Deep Green) and announcing (Vivid Green) surfaces */
	&.on-dark {
		background-color: var(--ink-inverse);
		color: var(--ink);

		&:hover {
			background-color: var(--cloud);
		}

		&.outline {
			background-color: transparent;
			color: var(--ink-inverse);
			border-color: var(--ink-inverse);

			&:hover {
				background-color: color-mix(in srgb, var(--ink-inverse) 12%, transparent);
			}
		}
	}

	&:focus-visible {
		outline: 2px solid var(--focus);
		outline-offset: 3px;
	}
}

/* HubSpot form submit — the primary treatment, written out.

   It does NOT inherit it. HubSpot renders the submit as
   class="hs-button primary large", so it carries a `primary` class and looks
   like it should already match — but every primary rule in this file is nested
   under .btn, and the submit has no .btn.

   That used to mean it took btn_shape alone and rendered as a tracked-caps
   micro button while the page's own CTAs rendered as labels. It no longer
   does: btn_shape IS the label treatment now, so the submit gets the size, the
   weight, the case and the height for free, and only the colour has to be
   restated here.

   Do not write a HubL tag inside a comment in this file. CSS comment syntax
   means nothing to HubL: it parses the whole file first, so a tag named in
   prose is a real tag. This block originally spelled the set tag out and the
   upload failed with "Missing end tag: endset" — it passed preflight and the
   local preview, because both read the CSS after HubL would have run.

   The colour and the size are ONE decision. White on Vivid Green is 3.34:1,
   which clears the 3:1 bar only because 18.66px at 700 counts as large-scale
   text; as normal text it fails 4.5:1. The outgoing --action-ink was 5.22:1
   and legal at any size, so dropping the size while keeping the white label
   would trade a brand bug for an accessibility one. Move both or neither. */
input[type="submit"].hs-button {
	
	min-height: 46px;
	padding: 0 16px;
	border-radius: var(--radPill);
	font-family: var(--inter);
	font-size: 1.866rem;
	font-weight: var(--bold);
	letter-spacing: 0;
	text-transform: none;
	line-height: 1.25;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	gap: 8px;
	text-align: center;
	box-sizing: border-box;
	border: 1px solid transparent;

	background-color: var(--action);
	color: var(--ink-inverse);

	/* 28, where .btn.primary is 16. NOT drift, and not a value to reconcile:
	   a form submit is almost always a single short word, and "Submit" at 16
	   came out 98 wide against a 46 tall pill on a 999 radius — stubby enough
	   that the pill read as a mistake. 28 takes it to 120. The page CTAs keep
	   the comp-measured 16, because their labels are long enough already
	   ("Book a Demo" is 178) and the comps measure them at that.

	   Height, weight, size and colour stay identical to .btn.primary — this is
	   the only property the two are allowed to disagree on. */
	padding: 0 28px;

	&:hover {
		background-color: color-mix(in srgb, var(--action) 88%, var(--ink));
	}

	&:focus-visible {
		outline: 2px solid var(--focus);
		outline-offset: 3px;
	}
}


/* --- Disabled --------------------------------------------------------------

   Replaces the dead rule near the top of this file. That one declared
   background --stone with a --stone-deep label and never applied to anything:
   .hs-button:disabled is (0,2,0), while input[type="submit"].hs-button is
   (0,2,1) AND later in the file, so it re-set both properties. Verified on the
   live page — setting disabled=true changed nothing at all: background stayed
   Vivid Green, label stayed white, cursor stayed pointer, and the hover
   darkening still fired.

   The old pairing was also unusable. --stone #D6DCD8 against --stone-deep
   #7F9185 computes to 2.4:1. Disabled controls are exempt from WCAG 1.4.3, so
   that would not have failed an audit, but a label nobody can read is still a
   defect. Recomputed for the replacement:

     --slate  #5E6863 on --stone #D6DCD8 = 4.15:1
     --ink    #171B19 on --stone #D6DCD8 = 12.50:1

   Slate is the choice. The submit label is 18.66px at 700, which WCAG counts
   as large-scale text, so the bar is 3:1 and 4.15 clears it with margin. Ink
   at 12.5 reads as fully active and defeats the point of the state.

   TOKEN CONCERN, not resolved here: --stone's own comment in _variables.css
   reads "DECORATIVE dividers only — never meaning." A disabled state is
   meaning, so this borrows a token against its stated role. There is no
   --surface-disabled or --ink-disabled in the palette. Flagged rather than
   invented; if a disabled role is ever added, this block moves onto it.

   Placed last on purpose. The .btn arm computes to (0,4,0), which TIES
   .btn.on-dark.outline:hover, and source order breaks the tie. Moving this
   block earlier in the file silently reactivates the hover on a disabled
   outline button. */

/* (0,3,2) for the submit — beats input[type="submit"].hs-button:hover at
   (0,3,1) on element count. The nested hover arm is (0,4,2). */
form input[type="submit"].hs-button:disabled,
form input[type="submit"].hs-button[aria-disabled="true"],
form .hs-button:disabled,
form .hs-button[aria-disabled="true"],
/* :is() carries the specificity of its heaviest argument, .on-dark.outline at
   (0,2,0), so each .btn arm totals (0,4,0). */
.btn:is(.primary, .secondary, .on-dark, .on-dark.outline):disabled,
.btn:is(.primary, .secondary, .on-dark, .on-dark.outline)[aria-disabled="true"] {
	background-color: var(--stone);
	border-color: var(--stone);
	color: var(--slate);
	cursor: not-allowed;

	/* Hover must be revoked explicitly. The enabled hover rules still match a
	   disabled control — :disabled does not suppress :hover — so without this
	   a dead button still darkens under the pointer and reads as live. */
	&:hover {
		background-color: var(--stone);
		border-color: var(--stone);
		color: var(--slate);
	}

	/* :focus-visible is deliberately NOT touched. A disabled control stays
	   focusable in some user agents, and the ring has to survive. */
}

	/* .srt, which the menu toggle's label sits in. */
	/* For content that needs to be visually hidden but stay visible for screenreaders */

.show-for-sr, .srt {
	border: 0 !important;
	clip: rect(0, 0, 0, 0) !important;
	height: 1px !important;
	overflow: hidden !important;
	padding: 0 !important;
	position: absolute !important;
	white-space: nowrap !important;
	width: 1px !important;
}

@media (max-width: 767px) {
	.show-for-sr--mobile, .srt-mobile {
		border: 0 !important;
		clip: rect(0, 0, 0, 0) !important;
		height: 1px !important;
		overflow: hidden !important;
		padding: 0 !important;
		position: absolute !important;
		white-space: nowrap !important;
		width: 1px !important;
	}
}

/* Colour utilities are generated from a single list so text/bg/fill can
   never drift out of sync — the old _helper.css had three hand-written
   blocks and a typo (--gra4300) that left .text-gray400, .bg-gray400 and
   .fill-gray400 pointing at an undefined variable. */






.text-surface { color: var(--surface); }
.bg-surface   { background-color: var(--surface); }
.fill-surface { fill: var(--surface); }

.text-surface-quiet { color: var(--surface-quiet); }
.bg-surface-quiet   { background-color: var(--surface-quiet); }
.fill-surface-quiet { fill: var(--surface-quiet); }

.text-surface-warm { color: var(--surface-warm); }
.bg-surface-warm   { background-color: var(--surface-warm); }
.fill-surface-warm { fill: var(--surface-warm); }

.text-surface-panel { color: var(--surface-panel); }
.bg-surface-panel   { background-color: var(--surface-panel); }
.fill-surface-panel { fill: var(--surface-panel); }

.text-surface-announcing { color: var(--surface-announcing); }
.bg-surface-announcing   { background-color: var(--surface-announcing); }
.fill-surface-announcing { fill: var(--surface-announcing); }

.text-surface-declaring { color: var(--surface-declaring); }
.bg-surface-declaring   { background-color: var(--surface-declaring); }
.fill-surface-declaring { fill: var(--surface-declaring); }

.text-ink-primary { color: var(--ink-primary); }
.bg-ink-primary   { background-color: var(--ink-primary); }
.fill-ink-primary { fill: var(--ink-primary); }

.text-ink-secondary { color: var(--ink-secondary); }
.bg-ink-secondary   { background-color: var(--ink-secondary); }
.fill-ink-secondary { fill: var(--ink-secondary); }

.text-ink-inverse { color: var(--ink-inverse); }
.bg-ink-inverse   { background-color: var(--ink-inverse); }
.fill-ink-inverse { fill: var(--ink-inverse); }

.text-ink-inverse-secondary { color: var(--ink-inverse-secondary); }
.bg-ink-inverse-secondary   { background-color: var(--ink-inverse-secondary); }
.fill-ink-inverse-secondary { fill: var(--ink-inverse-secondary); }

.text-ink-accent { color: var(--ink-accent); }
.bg-ink-accent   { background-color: var(--ink-accent); }
.fill-ink-accent { fill: var(--ink-accent); }

.text-action { color: var(--action); }
.bg-action   { background-color: var(--action); }
.fill-action { fill: var(--action); }

.text-action-ink { color: var(--action-ink); }
.bg-action-ink   { background-color: var(--action-ink); }
.fill-action-ink { fill: var(--action-ink); }

.text-signal { color: var(--signal); }
.bg-signal   { background-color: var(--signal); }
.fill-signal { fill: var(--signal); }

.text-signal-ink { color: var(--signal-ink); }
.bg-signal-ink   { background-color: var(--signal-ink); }
.fill-signal-ink { fill: var(--signal-ink); }

.text-line { color: var(--line); }
.bg-line   { background-color: var(--line); }
.fill-line { fill: var(--line); }

.text-line-functional { color: var(--line-functional); }
.bg-line-functional   { background-color: var(--line-functional); }
.fill-line-functional { fill: var(--line-functional); }

.text-error { color: var(--error); }
.bg-error   { background-color: var(--error); }
.fill-error { fill: var(--error); }

.text-success { color: var(--success); }
.bg-success   { background-color: var(--success); }
.fill-success { fill: var(--success); }

.text-warning { color: var(--warning); }
.bg-warning   { background-color: var(--warning); }
.fill-warning { fill: var(--warning); }

.text-deep-green { color: var(--deep-green); }
.bg-deep-green   { background-color: var(--deep-green); }
.fill-deep-green { fill: var(--deep-green); }

.text-evergreen { color: var(--evergreen); }
.bg-evergreen   { background-color: var(--evergreen); }
.fill-evergreen { fill: var(--evergreen); }

.text-vivid-green { color: var(--vivid-green); }
.bg-vivid-green   { background-color: var(--vivid-green); }
.fill-vivid-green { fill: var(--vivid-green); }

.text-lime { color: var(--lime); }
.bg-lime   { background-color: var(--lime); }
.fill-lime { fill: var(--lime); }

.text-warm-white { color: var(--warm-white); }
.bg-warm-white   { background-color: var(--warm-white); }
.fill-warm-white { fill: var(--warm-white); }

.text-ink { color: var(--ink); }
.bg-ink   { background-color: var(--ink); }
.fill-ink { fill: var(--ink); }

.text-cloud { color: var(--cloud); }
.bg-cloud   { background-color: var(--cloud); }
.fill-cloud { fill: var(--cloud); }

.text-sand { color: var(--sand); }
.bg-sand   { background-color: var(--sand); }
.fill-sand { fill: var(--sand); }

.text-community-mist { color: var(--community-mist); }
.bg-community-mist   { background-color: var(--community-mist); }
.fill-community-mist { fill: var(--community-mist); }

.text-stone { color: var(--stone); }
.bg-stone   { background-color: var(--stone); }
.fill-stone { fill: var(--stone); }

.text-stone-deep { color: var(--stone-deep); }
.bg-stone-deep   { background-color: var(--stone-deep); }
.fill-stone-deep { fill: var(--stone-deep); }

.text-slate { color: var(--slate); }
.bg-slate   { background-color: var(--slate); }
.fill-slate { fill: var(--slate); }

.text-community-blue { color: var(--community-blue); }
.bg-community-blue   { background-color: var(--community-blue); }
.fill-community-blue { fill: var(--community-blue); }

.text-community-clay { color: var(--community-clay); }
.bg-community-clay   { background-color: var(--community-clay); }
.fill-community-clay { fill: var(--community-clay); }

.text-golden-ochre { color: var(--golden-ochre); }
.bg-golden-ochre   { background-color: var(--golden-ochre); }
.fill-golden-ochre { fill: var(--golden-ochre); }

.text-vivid-violet { color: var(--vivid-violet); }
.bg-vivid-violet   { background-color: var(--vivid-violet); }
.fill-vivid-violet { fill: var(--vivid-violet); }

.text-blue-mist { color: var(--blue-mist); }
.bg-blue-mist   { background-color: var(--blue-mist); }
.fill-blue-mist { fill: var(--blue-mist); }

.text-clay-mist { color: var(--clay-mist); }
.bg-clay-mist   { background-color: var(--clay-mist); }
.fill-clay-mist { fill: var(--clay-mist); }

.text-ochre-mist { color: var(--ochre-mist); }
.bg-ochre-mist   { background-color: var(--ochre-mist); }
.fill-ochre-mist { fill: var(--ochre-mist); }

.text-violet-mist { color: var(--violet-mist); }
.bg-violet-mist   { background-color: var(--violet-mist); }
.fill-violet-mist { fill: var(--violet-mist); }

.text-cincGreen { color: var(--cincGreen); }
.bg-cincGreen   { background-color: var(--cincGreen); }
.fill-cincGreen { fill: var(--cincGreen); }

.text-electricGreen { color: var(--electricGreen); }
.bg-electricGreen   { background-color: var(--electricGreen); }
.fill-electricGreen { fill: var(--electricGreen); }

.text-forestGreen { color: var(--forestGreen); }
.bg-forestGreen   { background-color: var(--forestGreen); }
.fill-forestGreen { fill: var(--forestGreen); }

.text-blackGreen { color: var(--blackGreen); }
.bg-blackGreen   { background-color: var(--blackGreen); }
.fill-blackGreen { fill: var(--blackGreen); }

.text-black { color: var(--black); }
.bg-black   { background-color: var(--black); }
.fill-black { fill: var(--black); }

.text-gray900 { color: var(--gray900); }
.bg-gray900   { background-color: var(--gray900); }
.fill-gray900 { fill: var(--gray900); }

.text-gray700 { color: var(--gray700); }
.bg-gray700   { background-color: var(--gray700); }
.fill-gray700 { fill: var(--gray700); }

.text-gray500 { color: var(--gray500); }
.bg-gray500   { background-color: var(--gray500); }
.fill-gray500 { fill: var(--gray500); }

.text-gray400 { color: var(--gray400); }
.bg-gray400   { background-color: var(--gray400); }
.fill-gray400 { fill: var(--gray400); }

.text-gray300 { color: var(--gray300); }
.bg-gray300   { background-color: var(--gray300); }
.fill-gray300 { fill: var(--gray300); }

.text-gray200 { color: var(--gray200); }
.bg-gray200   { background-color: var(--gray200); }
.fill-gray200 { fill: var(--gray200); }

.text-gray100 { color: var(--gray100); }
.bg-gray100   { background-color: var(--gray100); }
.fill-gray100 { fill: var(--gray100); }

.text-white { color: var(--white); }
.bg-white   { background-color: var(--white); }
.fill-white { fill: var(--white); }


.fill { position: absolute; inset: 0; }
.cover { object-fit: cover; width: 100%; height: 100%; }

.blend-multiply { mix-blend-mode: multiply; }
.blend-color { mix-blend-mode: color; }

	/* Response motion — the spotlight bloom and the two-arrow throw the header
	   CTA emits, plus .lift, .nudge, .zoom-frame and the underline pair, which
	   the chrome does not use. Included whole rather than picked over, for the
	   reason in the header comment.

	   LAST, exactly as in main.css. Several of its rules tie on specificity
	   with what comes before and win on source order; move it up and the
	   button stops blooming. */
	/* Interaction motion — the pointer-state vocabulary.
   ===================================================================

   WHAT THIS FILE IS FOR, and what it deliberately is not.

   _animations.css owns ARRIVAL: a section entering the viewport, once, on
   scroll. This file owns RESPONSE: what a surface does while someone is
   pointing at it, pressing it, or tabbing through it. The two never touch the
   same properties on the same element, which is why they are separate files
   rather than one — arrival writes `opacity` and a y-translate and then stops
   forever; response writes a transform, a shadow and a colour, over and over.

   Nothing here is gated on html.js. It does not need to be: every rule below
   is a hover, focus or active state that starts from the FINAL appearance and
   moves away from it. With no JS, no CSS transitions, or a hostile browser,
   the page still renders exactly right and simply does not move. That is the
   opposite of the fail-blank hazard _animations.css had to be gated against,
   and the distinction is worth keeping straight: park-then-reveal needs a
   guarantee something will un-park it, response never does.

   These are CLASSES, not element rules, with one exception noted below —
   buttons, which are styled as elements. A
   module opts a surface in; nothing is imposed on markup that did not ask.
   Every class also carries an escape hatch as a custom property, so a module
   can retune the distance or the shadow without redeclaring the transition
   and drifting out of the family.

   ONE reduced-motion block, at the foot, covering everything. Do not add a
   local guard next to a rule — the whole point of collecting these here is
   that there is one place to check.


   THE ONE RULE THAT IS NOT NEGOTIABLE

     A reveal class and a response class never go on the same element.

   Not a style preference — a transition-delay problem with no CSS way out.
   _animations.css staggers a cascade with
   `.fadeup-cascade > *:nth-child(n) { transition-delay: calc(n * 0.12s) }`,
   which is (0,2,0) and outranks anything a single class here can say. That
   delay is not per-property: it applies to every property the element
   transitions, forever, not just during the reveal. Put .lift on the fortieth
   child of a cascade and its hover answers 4.8 seconds late.

   So for a grid of cards that also lift:

     .cards.fadeup          on the grid   — the grid arrives as one gesture
     .lift                  on each card  — and each card answers the pointer

   .fadeup-cascade is for lists with NO pointer state: the stat row, a logo
   wall, a directory of headings, footer columns. Where a grid genuinely wants
   both, the reveal goes on the grid and .lift goes on a wrapper INSIDE the
   card, never on the direct child the cascade is counting.


   WHY HOVER IS INSIDE @media (hover: hover).

   A tap on a touch screen leaves :hover applied until something else is
   tapped. Without the query, tapping a card on a phone leaves it stuck 2px in
   the air with a shadow under it, and it stays that way while the next page
   loads. `pointer: fine` is on the query too, so a stylus or a trackpad gets
   the state and a coarse touch pointer does not.

   :focus-within and :focus-visible are OUTSIDE that query on purpose. A
   keyboard is a fine pointer's peer, not a mouse's dependant, and a hybrid
   device — a laptop with a touch screen, a tablet with a keyboard — resolves
   `hover: hover` in ways that must never decide whether focus is visible. */


/* ============ 1. LIFT ============
   A surface that rises under the pointer: cards, panels, whole-tile links.

   -2px, not more. On a 300-wide card that is a shift of well under 1% and it
   reads as the card coming forward; at -8px it reads as the card jumping,
   which is a different and louder register than the deck's.

   The shadow is a variable rather than a value because a card's RESTING
   elevation decides which one is a step up. A card sitting flat on the page
   rises to --shadowRaised. A card that already carries --shadowRaised has
   nowhere to go, so it sets --liftShadow: var(--shadowOverlay) locally and
   the family still holds.

   :focus-within is the keyboard's version of the same event — tabbing to a
   link inside a card should move the card, or a keyboard user gets a focus
   ring floating on a surface that never acknowledged them. It is on the same
   rule rather than a separate one so the two states can never drift. */
.lift {
	--liftShadow: var(--shadowRaised);

	transition:
		transform  var(--durHover) var(--ease),
		box-shadow var(--durHover) var(--ease);
}

@media (hover: hover) and (pointer: fine) {
	.lift:hover {
		transform: translateY(var(--lift));
		box-shadow: var(--liftShadow);
	}
}

.lift:focus-within {
	transform: translateY(var(--lift));
	box-shadow: var(--liftShadow);
}


/* ============ 2. BUTTONS — SPOTLIGHT, THROW, PRESS ============
   The one place this file styles ELEMENTS rather than classes. A button that
   does not answer is the most-noticed missing interaction on a site, and
   making it opt-in guarantees some module somewhere forgets.

   Three things happen at once, and they are separable on purpose:

     SPOTLIGHT   a circular fill blooms from wherever the pointer entered.
                 Pure CSS; js/button-motion.js only writes two custom
                 properties, and the defaults below mean the effect is
                 complete and centred with that file absent.
     THROW       the arrow leaves to the right and a second one arrives from
                 the left, clipped by a one-em window. Needs the markup that
                 macros/button.html emits; a button with no arrow simply does
                 not do this half.
     PRESS       the control sinks. Down in --durPress, back up in --durHover:
                 a real button bottoms out the instant it is touched and
                 springs back over a longer arc, and matching the two makes it
                 feel like glue.

   There is no lift here any more. It was two pixels, it was correct, and it
   was the thing that read as polite rather than as an answer.


   THE FILL IS DARKER THAN THE BUTTON, AND THAT IS AN ACCESSIBILITY DECISION
   RATHER THAN A TASTE ONE. White on Vivid Green is 3.34:1 and legal only
   because 18.66px at 700 counts as large-scale text, which drops the bar from
   4.5:1 to 3:1 — a margin of 0.34 that _buttons.css spends three paragraphs
   protecting. A lighter fill would eat it. Measured in the browser off the
   computed ::before, compositing the translucent one over its own ground:

     primary            #067F45 vs white                     5.09:1
     secondary          #E8F1EC vs Evergreen                  5.72:1
     on-dark            #F3F5F2 vs Ink                       15.87:1
     on-dark.outline    white 16% over Deep Green = #294F4A   9.13:1

   Every one clears 4.5:1, so mid-bloom the label is AA at ANY size: the
   button is more legible under the pointer than at rest. Re-run these if a
   fill moves — the resting 3.34 has no room to give. Never Lime:
   _variables.css is direct that the signal token is not interaction chrome.


   WHY THE RESTING BACKGROUND IS PINNED BACK ON HOVER. _buttons.css already
   darkens background-color on hover. Left alone it fires in 0.12s and the
   bloom then arrives on top of a colour that has already changed, which
   destroys the effect — you see a colour change, then a circle in a third
   colour. Each variant below therefore restates its RESTING background at
   :hover, at the specificity that ties _buttons.css and wins on source order.
   The border colour is deliberately left to change, since that is an edge
   rather than a fill and the two do not fight.

   Nothing here is gated on html.js. All of it starts from the button's final
   appearance and moves away from it, so with no script, no transitions, or a
   hostile browser the button renders exactly right and simply does not move. */

.btn {
	position: relative;
	z-index: 0;
	overflow: hidden;

	/* Centre, so the bloom is complete and sensible with the script absent —
	   and so KEYBOARD FOCUS, which has no pointer position to read, blooms
	   from somewhere deliberate rather than from the top-left corner. */
	--fxX: 50%;
	--fxY: 50%;
	--fxFill: color-mix(in srgb, var(--action) 74%, var(--ink));

	transition:
		background-color var(--durQuick) var(--ease),
		border-color     var(--durQuick) var(--ease),
		color            var(--durQuick) var(--ease),
		transform        var(--durHover) var(--ease);
}

.btn.secondary { --fxFill: var(--community-mist); }
.btn.on-dark   { --fxFill: var(--cloud); }
.btn.on-dark.outline { --fxFill: color-mix(in srgb, var(--ink-inverse) 16%, transparent); }

/* A fixed circle that is SCALED, not a circle whose radius animates. Scale is
   composited; width and height relayout the pseudo-element every frame.

   320px at 1.9 is 608 across, which covers the widest button on the site —
   "Book a Demo" at 178 — from its farthest corner. A shorter button simply
   overshoots, and that costs nothing because the parent clips it.

   z-index -1 inside a z-index 0 parent is what puts the fill above the
   background and below the label. The 0 on .btn is load-bearing: without it
   the pseudo-element escapes to the page's stacking context and vanishes
   behind the section. */
.btn::before {
	content: "";
	position: absolute;
	z-index: -1;
	left: var(--fxX);
	top: var(--fxY);
	width: 320px;
	height: 320px;
	margin: -160px 0 0 -160px;
	border-radius: 50%;
	background: var(--fxFill);
	transform: scale(0);

	/* LEAVING. Declared on the resting state, so it is what plays when the
	   pointer goes — the arriving pair live on :hover and :focus-visible
	   below. Short, and on --easeOut rather than --easeBloom: on the way out
	   the overshoot uncovers first, so there is no invisible stretch to spread
	   time over and an ease-in-out would just feel reluctant. */
	transition: transform var(--durBloomOut) var(--easeOut);
}

@media (hover: hover) and (pointer: fine) {
	/* ARRIVING. --easeBloom, NOT --ease. The circle overshoots to 1.9 so it
	   reaches the far corner from a near-corner entry, but the button is
	   covered at about 1.15 — 60.5% of the way. On the theme's expo-out that
	   fraction lands at 94ms and the remaining 606ms grows past the edges
	   where nobody can see it, so the fill read as instant no matter what
	   --durBloom said. On --easeBloom it lands at 376ms. See _variables.css
	   for the arithmetic; the duration was never the problem. */
	.btn:hover::before {
		transform: scale(1.9);
		transition: transform var(--durBloom) var(--easeBloom);
	}

	.btn.primary:hover,
	input[type="submit"].hs-button:hover { background-color: var(--action); }
	.btn.secondary:hover { background-color: transparent; }
	.btn.on-dark:hover { background-color: var(--ink-inverse); }
	.btn.on-dark.outline:hover { background-color: transparent; }
}

/* Outside the hover query: a keyboard is a fine pointer's peer, not its
   dependant, and on a hybrid device `hover: hover` must never be what decides
   whether focus is answered. */
.btn:focus-visible::before {
	transform: scale(1.9);
	transition: transform var(--durBloom) var(--easeBloom);
}


/* --- THROW ---------------------------------------------------------------
   A one-em window clipping two copies of the arrow. 160% each way rather than
   100% so each clears the window completely before the other lands —
   at 100% both are on screen at the handover and it reads as a smear.

   No flex context is set here. btn_shape in _buttons.css makes every .btn an
   inline-flex row with a gap, which it has to be for the label treatment to
   line up anyway — so the window is a flex item and sits on the optical centre
   with nothing to arrange. This block briefly carried a `.btn:has(.btn-arrow)`
   rule for that, back when only .primary was inline-flex; it is gone rather
   than kept as a belt, because a second place setting display on a button is
   exactly how the two treatments diverged in the first place. */
.btn-arrow {
	position: relative;
	display: inline-block;
	width: 1em;
	height: 1em;
	overflow: hidden;
	flex: 0 0 auto;
}

.btn-arrow > i {
	position: absolute;
	inset: 0;
	display: grid;
	place-items: center;
	transition: transform var(--durThrow) var(--ease);
}

.btn-arrow > i:last-child { transform: translateX(-160%); }

@media (hover: hover) and (pointer: fine) {
	.btn:hover .btn-arrow > i:first-child { transform: translateX(160%); }
	.btn:hover .btn-arrow > i:last-child  { transform: translateX(0); }
}

.btn:focus-visible .btn-arrow > i:first-child { transform: translateX(160%); }
.btn:focus-visible .btn-arrow > i:last-child  { transform: translateX(0); }


/* --- PRESS ---------------------------------------------------------------
   :active fires on touch as well as mouse, and should — a tap SHOULD sink the
   button. It is outside the hover query for exactly that reason.

   .985 rather than .95: at this size the scale is felt more than seen, which
   is what a press is. A visible shrink reads as the button changing size.

   The transition narrows to transform alone for the duration of the press.
   Colour is already where hover left it, so nothing is lost, and it keeps the
   80ms off properties that would flicker at that speed. */
.btn:active,
input[type="submit"].hs-button:active {
	transform: scale(.985);
	transition: transform var(--durPress) var(--ease);
}


/* --- THE HUBSPOT FORM SUBMIT --------------------------------------------
   IT CANNOT HAVE THE SPOTLIGHT, AND NO AMOUNT OF CSS WILL CHANGE THAT.
   HubSpot renders the submit as input[type="submit"], and a replaced element
   has no ::before to bloom and no room for a child element to throw. So this
   one control takes a LINEAR sweep instead, painted as a background-image,
   which a replaced element does accept.

   Same fill, same duration, same easing, so it belongs to the family — it
   simply arrives from the left edge rather than from the pointer. That is a
   real inconsistency and it is the cheapest one available: the alternative is
   giving every button on the site the linear sweep so that the one control
   nobody looks at can keep up.

   background-size on a two-stop gradient rather than a scaled layer, because
   there is no layer to scale. It costs a paint per frame on a box that is
   never more than 200px wide. */
input[type="submit"].hs-button {
	--fxFill: color-mix(in srgb, var(--action) 74%, var(--ink));

	background-image: linear-gradient(var(--fxFill), var(--fxFill));
	background-repeat: no-repeat;
	background-position: left center;
	background-size: 0% 100%;

	/* LEAVING, same split as the circle. The whole list is restated on the
	   arriving rules below rather than only background-size, because
	   `transition` is a shorthand: naming one property there would drop the
	   colour and transform transitions for the duration of the hover. */
	transition:
		background-color var(--durQuick)    var(--ease),
		background-size  var(--durBloomOut) var(--easeOut),
		color            var(--durQuick)    var(--ease),
		transform        var(--durHover)    var(--ease);
}

/* ARRIVING. A linear wipe does not overshoot — 100% is exactly covered — so
   this one never had the invisible-growth problem the circle had. It takes
   --easeBloom anyway: the two are one gesture in different geometry, and on
   an expo-out the wipe still delivered most of its width in the first tenth
   and read as a flash beside a 376ms bloom. */


@media (hover: hover) and (pointer: fine) {
	input[type="submit"].hs-button:hover { 
	background-size: 100% 100%;
	transition:
		background-color var(--durQuick) var(--ease),
		background-size  var(--durBloom) var(--easeBloom),
		color            var(--durQuick) var(--ease),
		transform        var(--durHover) var(--ease);
 }
}

input[type="submit"].hs-button:focus-visible { 
	background-size: 100% 100%;
	transition:
		background-color var(--durQuick) var(--ease),
		background-size  var(--durBloom) var(--easeBloom),
		color            var(--durQuick) var(--ease),
		transform        var(--durHover) var(--ease);
 }


/* --- DISABLED ------------------------------------------------------------
   A dead control must not answer at all. _buttons.css already revokes the
   colour hover for these; this revokes the movement and the fill, and has to
   name the same selectors, because a button that still blooms and sinks under
   the finger reads as live and broken rather than as unavailable. */
.btn:disabled,
.btn[aria-disabled="true"],
input[type="submit"].hs-button:disabled,
input[type="submit"].hs-button[aria-disabled="true"] {
	background-image: none;

	&::before { content: none; }

	&:hover,
	&:active,
	&:focus-visible {
		transform: none;
		background-size: 0% 100%;

		.btn-arrow > i:first-child { transform: none; }
		.btn-arrow > i:last-child { transform: translateX(-160%); }
	}
}


/* ============ 3. NUDGE ============
   An arrow or chevron that travels toward where it is about to take you.

   Goes on the ICON, and fires from the enclosing interactive element, so the
   whole link is the target rather than the 12px glyph. solution-cards
   measured this at 3px and it is now --nudge; the value is small on purpose,
   because the icon is small and 3px on a 12px glyph is already a quarter of
   its own width.

   :where() on the ancestor keeps this at (0,1,0) — one class — so any module
   that needs a different distance can override it with a plain selector
   instead of having to out-specify a compound one.

   --nudgeAxis lets a chevron that points DOWN reuse the whole rule:
   .nudge { --nudgeAxis: 0, var(--nudge) } and it travels on y instead. */
.nudge {
	--nudgeAxis: var(--nudge), 0;

	display: inline-block;
	transition: transform var(--durHover) var(--ease);
}

@media (hover: hover) and (pointer: fine) {
	:where(a, button, .lift):hover .nudge {
		transform: translate(var(--nudgeAxis));
	}
}

:where(a, button, .lift):focus-visible .nudge,
:where(.lift):focus-within .nudge {
	transform: translate(var(--nudgeAxis));
}


/* ============ 4. ZOOM FRAME ============
   An image that grows inside a frame that clips it.

   WHAT MAY GO IN THE FRAME, and this is the part the class shipped without.
   A photograph, footage, or artwork drawn to a safe area someone specified.
   NOT a UI screenshot, a device mockup, a chart, a logo or an embedded page —
   anything whose framing was INHERITED rather than commissioned. There the
   boundary is information and scale() deletes it from the outside in. The test
   is not "is it a picture", it is "did anyone decide where its edge would
   fall". A screenshot's margin was decided by the app being captured.

   THE ARITHMETIC, so nobody re-derives it. scale(1.04) about the centre pushes
   each edge out by 2% of the frame's width and hides 1.923% of the image
   behind the clip, per side. It is a PROPORTION, so it does not wash out at a
   smaller viewport, and it is subtractive — the frame is cover, so the zoom
   can only ever take.

   MEASURED, on the frame that broke. solution-cards' wide .card-media is about
   1126 CSS px at 1440, so about 22px a side — roughly 48px of a 2390-wide
   screenshot whose table rule sits 22px in. That card has NO horizontal crop
   at rest, because cover matches it on width, so the hover manufactured the
   clip out of nothing and handed it back when the pointer left. A rest crop is
   at least stable and AUTHORABLE: the author sees it and composes for it.
   Nobody can compose for a crop that exists only while a pointer is over it.

   A SMALLER LOCAL --zoom DOES NOT ANSWER THIS. At 1.01 the same wide card
   still takes about 6px a side and the table rule is still 22 source px in. A
   smaller number reduces the travel, not the kind of loss. If the framing is
   content, the answer is a different treatment, not a gentler one.

   THE CHEAPEST TEST IS THE ALT TEXT, and the author writes it anyway. An alt
   that enumerates named parts — "association, role, sender, type and status" —
   is the author saying the framing is the content. An alt that describes a
   scene, or is empty, is not. connect-cards ships both in one card: the
   background photograph is alt="" and a direct child, so it zooms; the app
   screenshot takes real alt text and sits one level down inside .card-device,
   so `> img` never reaches it. That nesting is LOAD-BEARING rather than
   tidy-up-able — flatten the wrapper and the mockup starts losing its bezel.

   The frame owns the overflow, the child owns the transform. Splitting it that
   way is what stops the zoom pushing the layout around — the box never changes
   size, only its contents do. It does NOT relieve a frame of stating its own
   overflow: a slot that clips for its own reasons should say so, or the clip
   vanishes the day this class comes off. solution-cards learned that the hard
   way — it dropped its local overflow when it adopted this, and getting the
   class removed meant putting it back.

   Roughly twice --durHover. A photograph carries far more area than a 46px
   pill, and matching the button's speed makes it snap rather than settle;
   this is the one thing in the file allowed to take its time.

   --zoom is a SCALE, so the travel it produces depends on the frame. 1.04 on
   a 620-wide frame moves the far edge about 12px, the same order as a card
   lift. On a full-bleed band it will read as much more — set a smaller value
   on that frame rather than lowering the token for everyone.

   `backface-visibility` and the explicit translateZ are the standard fix for
   the text-and-edge shimmer WebKit produces when it rasterises a scaled
   layer; without them a scaled photograph's edge crawls during the tween. */
.zoom-frame {
	overflow: hidden;

	> img,
	> video,
	> picture > img {
		transition: transform calc(var(--durHover) * 2) var(--ease);
		backface-visibility: hidden;
		transform: translateZ(0);
	}
}

@media (hover: hover) and (pointer: fine) {
	:where(a, .lift, .zoom-frame):hover .zoom-frame > img,
	:where(a, .lift, .zoom-frame):hover .zoom-frame > video,
	:where(a, .lift, .zoom-frame):hover .zoom-frame > picture > img,
	.zoom-frame:hover > img,
	.zoom-frame:hover > video,
	.zoom-frame:hover > picture > img {
		transform: scale(var(--zoom));
	}
}


/* ============ 5. LINK UNDERLINES ============

   --- .underline-grow — the theme's existing treatment, finally named.

   1px to 2px on hover. Five modules had written this out locally before it
   had a class (solution-cards twice, promo-cards, feature-directory,
   statement-cards), with three different --underline-offset values between
   them. This is that pattern with one offset.

   The underline is present AT REST, which is what makes it safe anywhere,
   including inside a paragraph: WCAG 1.4.1 requires that a link in running
   text be distinguishable by something other than colour, and a permanent
   underline is that something.

   text-decoration-thickness animates in current Chrome, Safari and Firefox
   and snaps in anything older. Snapping is an acceptable degradation — the
   state change still happens, it just arrives all at once. */
.underline-grow {
	text-decoration: underline;
	text-decoration-thickness: 1px;
	text-underline-offset: .18em;
	transition: text-decoration-thickness var(--durQuick) var(--ease);
}

@media (hover: hover) and (pointer: fine) {
	.underline-grow:hover {
		text-decoration-thickness: 2px;
	}
}

.underline-grow:focus-visible {
	text-decoration-thickness: 2px;
}

/* --- .underline-wipe — an underline that draws itself in from the left.

   NOT FOR LINKS IN RUNNING TEXT. There is no underline at rest, so inside a
   paragraph this fails WCAG 1.4.1 the moment colour is the only thing marking
   the link. It is for lists of links that are obviously links from their
   placement — the nav pill, the footer columns, a directory of headings —
   where the grouping does the identifying that the underline would otherwise
   have to do. Use .underline-grow everywhere else.

   Drawn as a background gradient rather than a border or a pseudo-element
   because background-size is the only one of the three that animates cleanly
   across a wrapped line: a ::after underline on a link that breaks over two
   lines draws one bar under the last line only.

   currentColor, so it inherits the link's colour on every surface — Warm
   White, Sand, Deep Green — with nothing to keep in step. Explicitly not the
   Lime signal token: _variables.css is direct that Lime is semantic and must
   not become interaction chrome. */
.underline-wipe {
	text-decoration: none;
	background-image: linear-gradient(currentColor, currentColor);
	background-repeat: no-repeat;
	background-position: 0 100%;
	background-size: 0% 1px;
	padding-block-end: .12em;
	transition: background-size var(--durHover) var(--ease);
}

@media (hover: hover) and (pointer: fine) {
	.underline-wipe:hover {
		background-size: 100% 1px;
	}
}

.underline-wipe:focus-visible {
	background-size: 100% 1px;
}


/* ============ 6. REDUCED MOTION ============
   One block for the whole file.

   Note what it does NOT do: it never restores the resting appearance the way
   _animations.css's guard has to. Nothing here hides anything, so there is
   nothing to un-hide — the correct behaviour is simply that the states arrive
   without travel.

   So transforms are dropped and transitions removed, while the COLOUR and
   SHADOW changes are deliberately left alone. Someone who has asked for less
   motion has not asked for less feedback, and a button that no longer
   responds at all to being pressed is a worse outcome than one that responds
   instantly. The underline states stay for the same reason: a thickness
   change is not motion.

   The press keeps its scale removed but keeps :active meaningful through the
   colour change _buttons.css already owns. */
@media (prefers-reduced-motion: reduce) {
	.lift,
	.btn,
	input[type="submit"].hs-button,
	.nudge,
	.zoom-frame > img,
	.zoom-frame > video,
	.zoom-frame > picture > img,
	.underline-grow,
	.underline-wipe {
		transition: none;
	}

	.lift:hover,
	.lift:focus-within,
	.btn:hover,
	.btn:active,
	input[type="submit"].hs-button:hover,
	input[type="submit"].hs-button:active,
	:where(a, button, .lift):hover .nudge,
	:where(a, button, .lift):focus-visible .nudge,
	:where(.lift):focus-within .nudge,
	.zoom-frame:hover > img,
	.zoom-frame:hover > video,
	.zoom-frame:hover > picture > img,
	:where(a, .lift, .zoom-frame):hover .zoom-frame > img,
	:where(a, .lift, .zoom-frame):hover .zoom-frame > video,
	:where(a, .lift, .zoom-frame):hover .zoom-frame > picture > img {
		transform: none;
	}

	/* The wipe has no non-motion form — an underline that is 0% wide is no
	   underline. Show it in full rather than not at all, so the hover still
	   marks the link. */
	.underline-wipe:hover,
	.underline-wipe:focus-visible {
		background-size: 100% 1px;
	}
}
}