/*
Theme Name: DivIni Apex
Theme URI: https://divinisoft.com
Author: DivIni Software
Author URI: https://divinisoft.com
Description: AI-first engineering theme for DivIni Software. Daylight paper surfaces, two dark
inserts, a hero that contracts from night into day, and sections that stack like cards as you
scroll. Vivid cobalt / persimmon / butter accents on the brand teal.
Version: 2.0.0
Requires at least: 6.4
Requires PHP: 7.4
Tested up to: 6.7
Text Domain: divinisoft-apex
*/

@import url('https://fonts.googleapis.com/css2?family=Archivo:wdth,wght@75..125,400..900&family=Public+Sans:wght@300..700&family=DM+Mono:wght@300;400;500&display=swap');

/* ==========================================================================
   Design tokens
   ========================================================================== */
:root {
	--radius: 1.125rem;
	--radius-lg: 1.75rem;

	/* --- palette -----------------------------------------------------------
	   Layout/background treatment follows "Follow the Sun" (paper daylight,
	   ink night, hairline structure). The accent colours are swapped in from
	   "Nine, One": cobalt + persimmon + butter over the same brand teal. */
	--ink:        #101619;   /* night — footer, dark inserts              */
	--ink-raise:  #1A2226;   /* raised dark surface inside a dark section */
	--hero-ink:       #0C242C;   /* hero only — night pulled from the logo's teal, not neutral slate; lightened a step so the aurora ripples have more contrast to bloom against */
	--hero-ink-raise: #0F2B32;   /* hero's own raised surfaces (panel, code card) */
	--home-hero-ink:  #12303A;   /* the home page hero's ground alone — --hero-ink one step lighter (same hue, 11% to 15% lightness) */
	--teal:       #5DA0AC;   /* brand, sampled from the logo mark        */
	--teal-deep:  #2F7E8E;   /* brand at text contrast on paper          */
	--teal-ink:   #246275;   /* teal-deep only clears ~4.1:1 on --paper's
	                            mist (it was measured against --paper-hi's
	                            white, where it's 4.67:1) — under AA for
	                            small text. This is teal at 6:1+ on --paper
	                            itself, for small-text/label use directly
	                            on the mist background rather than a card. */
	--beam:       #2B34E0;   /* cobalt — the electric signal             */
	--sun:        #FF6A45;   /* persimmon — the warm signal / CTA        */
	--sun-deep:   #B4441F;   /* persimmon at text contrast on paper — same
	                            role --teal-deep plays for the teal. Plain
	                            --sun carries white text at only 2.8:1 and
	                            sits at 2.8:1 as text on paper, so anything
	                            warm that has to be *read* on a light
	                            surface uses this instead.               */
	--butter:     #F5CE5B;   /* third accent, used sparingly             */
	--paper:      #EDF2F1;   /* mist — daylight base                     */
	--paper-hi:   #FFFFFF;

	--ink-70: rgba(16,22,25,.70);
	--ink-45: rgba(16,22,25,.46);
	--lite-72: rgba(234,243,241,.72);
	--lite-45: rgba(234,243,241,.46);
	--hair-d: rgba(234,243,241,.20);   /* hairline for use on dark  */
	--hair-l: rgba(16,22,25,.10);      /* hairline for use on light */

	/* --- type ---------------------------------------------------------------
	   Archivo display / Public Sans body / DM Mono data — a deliberate swap
	   away from an Inter-everywhere default. */
	--font-display: 'Archivo', 'Arial Black', system-ui, sans-serif;
	--font-sans: 'Public Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
	--font-mono: 'DM Mono', ui-monospace, 'SF Mono', Menlo, Monaco, Consolas, monospace;

	/* Text floor: the smallest size anything on the site is set in, so fine
	   print stays readable on dense laptop screens and on phones. Mono text
	   and uppercase labels never go below --text-min; running text in the
	   body face (Public Sans) never below --text-min-sans, since at the same
	   size it reads smaller than the wide mono. Reach for these instead of a
	   smaller literal size. (Text inside SVG diagrams is sized by the
	   drawing's own scale, so it's checked per diagram, not here.) */
	--text-min: 0.75rem;         /* 12px at the default root */
	--text-min-sans: 0.8125rem;  /* 13px */

	/* --- scale --------------------------------------------------------------
	   One dial sizes the whole design: the root font-size (the `html` rule
	   under "Reset & base"). Every length in this file is written in rem
	   (px / 16), so at the default 16px root the site renders exactly as it
	   was designed and tuned, and when the root grows on a big screen,
	   everything grows with it in proportion: text, spacing, radii, icons and
	   this 80rem (1280px) content column.
	   - New sizes go in rem (24px = 1.5rem). px stays only where a number must
	     not scale: hairlines and 1-2px nudges, borders, outlines, shadows,
	     filters, the 999px "fully round" pill radius, and rules that style
	     SVG internals (lengths inside an SVG are user units, which already
	     scale with the drawing).
	   - Media queries are in em (980px = 61.25em), with the px value noted
	     beside each one. em in a media query always means the browser's
	     default font size, never the root's, so the dial can't move a
	     breakpoint; a reader who has set a larger default font size gets the
	     narrower layouts sooner, which is what that setting asks for.
	   - Numbers in main.js that mirror a CSS size go through its rem(). */
	--container: 80rem;
	--ease: cubic-bezier(.22,1,.36,1);
	--spring: cubic-bezier(.34,1.42,.44,1);

	/* --- semantic tokens (light default — most of the page) --------------- */
	--background: var(--paper);
	--foreground: var(--ink);
	--surface: var(--paper-hi);
	--card: var(--paper-hi);
	--popover: #F4F8F7;
	--muted: #E5ECEB;
	--muted-foreground: var(--ink-70);
	--accent: #E7F0EF;
	--accent-foreground: var(--teal-deep);
	--border: var(--hair-l);
	--ring: var(--beam);

	--brand-blue: var(--sun);
	--brand-purple: var(--beam);
	--brand-cyan: var(--teal);

	--gradient-brand: linear-gradient(100deg, var(--teal) 0%, var(--sun) 100%);
	--gradient-aurora:
		radial-gradient(60% 60% at 20% 20%, rgba(43,52,224,.32), transparent 60%),
		radial-gradient(60% 60% at 80% 30%, rgba(93,160,172,.30), transparent 60%),
		radial-gradient(60% 60% at 50% 90%, rgba(255,106,69,.26), transparent 60%);
	/* Hero-only variant of the aurora glow — kept separate from
	   --gradient-aurora so the capability-media uses above stay
	   untouched. White stands in for cobalt at low alpha (kept low: at full
	   strength it blows out the corner), teal keeps its position, persimmon
	   is new at bottom-centre. */
	--gradient-hero:
		radial-gradient(48% 42% at 16% 14%, rgba(255,255,255,.20), transparent 66%),
		radial-gradient(52% 48% at 84% 24%, rgba(93,160,172,.62), transparent 68%),
		radial-gradient(58% 46% at 50% 100%, rgba(255,106,69,.68), transparent 66%);

	--shadow-soft: 0 24px 60px -34px rgba(16,22,25,.30);
	--shadow-card: 0 40px 90px -46px rgba(16,22,25,.38), 0 2px 2px rgba(16,22,25,.04);

	/* hero stage reveal — horizontal-only inset (clip-path, not scale), so
	   the hero keeps full viewport height throughout and only narrows in
	   from the left/right edges as it contracts into a floating card.
	   Doubled again (.03 -> .06) so the contraction reads clearly against
	   the fixed .hero-stage-grid backdrop it now reveals on either side —
	   the inset is carrying the entire felt "something is happening"
	   moment on its own, so it needs to be visible without reading as a
	   jarring squeeze. */
	--shrink-x: .06;
	--pe: 0;
}

/* ==========================================================================
   Reset & base
   ========================================================================== */
*, *::before, *::after { box-sizing: border-box; }
/* The scale dial (see "scale" under Design tokens). It holds at 100% (the
   browser's default, 16px for nearly everyone) until the window is both
   wider than 1600px and taller than 840px, so every phone, tablet and laptop
   layout in this file renders exactly as tuned. Past that it grows with
   whichever side has less room to spare: +10% at 1920px wide (a Full HD
   screen at 100% Windows scaling), +20% at most (2240px+). The height term
   keeps the window at least 840px tall in the 16px design's own pixels,
   so a bigger dial never pushes a hero's button below the fold: the tallest
   hero (Payments) needs 837px to show its button on the first screen,
   About Us 807, the rest less (measured). The percentages are of the
   browser's default size, so a visitor who has set a larger default font
   never gets less than that. */
html {
	font-size: clamp(100%, min(50% + 0.5vw, 100vh * 16 / 840), 120%);
	scroll-behavior: smooth; -webkit-text-size-adjust: 100%; overflow-x: clip;
}
body {
	margin: 0;
	background: var(--background);
	color: var(--foreground);
	font-family: var(--font-sans);
	font-size: 1.03125rem;
	line-height: 1.6;
	font-weight: 400;
	-webkit-font-smoothing: antialiased;
	overflow-x: clip;
}
img { max-width: 100%; display: block; }
/* Inline icons carry their size as width/height attributes (px), which the
   scale dial can't reach. These restate every icon size the pages use in
   rem, so icons grow with the text around them. :where() keeps them at zero
   specificity: any component rule that sizes an icon still wins. A new icon
   size needs a line here too. */
:where(svg[width="12"][height="12"]) { width: 0.75rem; height: 0.75rem; }
:where(svg[width="14"][height="14"]) { width: 0.875rem; height: 0.875rem; }
:where(svg[width="15"][height="15"]) { width: 0.9375rem; height: 0.9375rem; }
:where(svg[width="16"][height="16"]) { width: 1rem; height: 1rem; }
:where(svg[width="18"][height="18"]) { width: 1.125rem; height: 1.125rem; }
:where(svg[width="20"][height="20"]) { width: 1.25rem; height: 1.25rem; }
:where(svg[width="21"][height="21"]) { width: 1.3125rem; height: 1.3125rem; }
:where(svg[width="22"][height="22"]) { width: 1.375rem; height: 1.375rem; }
:where(svg[width="24"][height="24"]) { width: 1.5rem; height: 1.5rem; }
:where(svg[width="28"][height="28"]) { width: 1.75rem; height: 1.75rem; }
:where(svg[width="32"][height="32"]) { width: 2rem; height: 2rem; }
/* Small print at the text floor (card footers, captions, chip labels) wraps
   "pretty": when a line has to break, the browser avoids leaving one word
   alone on the last line. Browsers without text-wrap: pretty just wrap as
   before. */
:where(figcaption, .run-foot, .router-foot, .recon-foot, .prov-foot, .ticket-foot,
	.chat-foot, .rag-panel-answer, .photo-duo-chip-label, .capability-media-caption,
	.twin-run-verdict, .role-list-note, .scope-cap) { text-wrap: pretty; }
a { color: inherit; text-decoration: none; }
ul { margin: 0; padding: 0; list-style: none; }
h1, h2, h3, h4, h5 {
	margin: 0; font-family: var(--font-display);
	font-variation-settings: 'wdth' 112, 'wght' 740;
	letter-spacing: -0.03em; line-height: 1.08; text-wrap: balance;
}
p { margin: 0; }
button { font-family: inherit; cursor: pointer; background: none; border: none; color: inherit; }
input, textarea { font-family: inherit; }
:focus-visible { outline: 2px solid var(--ring); outline-offset: 3px; border-radius: 0.375rem; }

.container { max-width: var(--container); margin: 0 auto; padding: 0 1.5rem; }
.text-gradient {
	background: var(--gradient-brand);
	-webkit-background-clip: text;
	background-clip: text;
	color: transparent;
}
.screen-reader-text {
	position: absolute; width: 1px; height: 1px; overflow: hidden;
	clip: rect(1px,1px,1px,1px); white-space: nowrap;
}
/* Keeps a hyphenated term like "AI-First" on one line. A plain hyphen is a
   break opportunity, so a narrow heading can strand "AI-" at a line end.
   Preferred over U+2011 (non-breaking hyphen): that glyph may be missing
   from the display face and fall back to a different font, and it stops
   the term matching a plain-text search. */
.nowrap { white-space: nowrap; }
/* Keeps a short phrase, such as a promise ("we will reply within 24 hours"),
   whole: if it can't share the line it moves to the next as a unit, instead
   of a phone breaking it mid-clause. Unlike .nowrap it still wraps inside
   itself on a screen too narrow to hold it, rather than running out of its
   box and being clipped. */
.keep-together { display: inline-block; }

/* ==========================================================================
   Eyebrow / section heading — a mono data-label, not a badge
   ========================================================================== */
.eyebrow {
	display: inline-flex; align-items: center; gap: 0.625rem;
	font-family: var(--font-mono); font-size: var(--text-min); font-weight: 500;
	letter-spacing: 0.16em; text-transform: uppercase;
	color: var(--teal-deep);
	margin-bottom: 1.25rem;
}
.eyebrow::before { content: ''; width: 1.375rem; height: 1px; background: currentColor; opacity: .55; }

.section { padding: 6rem 0; position: relative; }
.section-head { max-width: 45rem; margin: 0 auto 3.5rem; text-align: center; }
.section-head .eyebrow { justify-content: center; }
.section-head h2 { font-size: clamp(2rem, 3.4vw, 2.9rem); }
.section-head p { margin-top: 1.125rem; font-size: 1.1rem; color: var(--muted-foreground); }

/* ==========================================================================
   Buttons
   ========================================================================== */
.btn {
	display: inline-flex; align-items: center; justify-content: center; gap: 0.5625rem;
	padding: 0.875rem 1.75rem; border-radius: 999px; font-weight: 600; font-size: 0.9375rem;
	border: 1px solid transparent; transition: transform .3s var(--spring), box-shadow .3s var(--ease), background .25s, border-color .25s, color .25s;
	white-space: nowrap;
}
.btn:hover { transform: translateY(-2px); }
.btn svg { transition: transform .3s var(--ease); }
.btn:hover svg { transform: translateX(0.25rem); }
.btn-primary { background: var(--teal-deep); color: #fff; box-shadow: 0 16px 36px -18px rgba(47,126,142,.55); }
.btn-primary:hover { box-shadow: 0 20px 44px -16px rgba(47,126,142,.75); }
.btn-outline { background: var(--card); border-color: var(--border); color: var(--foreground); }
.btn-outline:hover { background: var(--accent); border-color: var(--teal); }
.btn-lg { padding: 1.0625rem 2.125rem; font-size: 1rem; }

/* ==========================================================================
   Header
   ========================================================================== */
.site-header {
	position: fixed; top: 0; left: 0; right: 0; z-index: 100;
	padding: 1.25rem 0;
	background: transparent;
	transition: padding .5s var(--ease), transform .35s var(--ease);
}
/* .is-hidden (initHeaderAutoHide() in main.js) -- mobile/tablet only, see
   that function's own comment for why. Media-gated here too, not just in
   the JS that adds the class, so a stray class surviving a resize back to
   desktop (or a build without JS) can never slide the always-visible
   desktop header away. */
@media (max-width: 61.25em) { /* 980px */
	.site-header.is-hidden { transform: translateY(-100%); }
}
/* The bar shrinks the moment .is-day lands, on every page alike, dark
   hero or not (initHeroHeader() in main.js toggles .is-day past a small
   scroll threshold, sitewide; the homepage's #heroStage-driven version
   below sets the same class). One unscoped rule instead of repeating it
   under .home and every dark-hero :has() list, because the shrink was
   never actually about the dark hero — only the color/transparency
   change further below is. Before this rule existed, only pages with a
   listed dark hero (plus the homepage) ever earned .is-day at all, so a
   plain page's header stayed at its tall default forever; see the
   .product-hero comment for the workaround that had to lean on instead. */
.site-header.is-day { padding: 0.625rem 0; }
.site-header.is-day .container { padding: 0.5625rem 1.5rem; }
/* The bar floats as a rounded, inset card instead of an edge-to-edge strip
   — .site-header itself is just a transparent positioning wrapper, and this
   glass pill is the only thing that actually paints. Site-wide default; the
   homepage overrides it below for the moment it's floating over the dark
   hero, before scroll brings it in. */
.site-header .container {
	display: flex; align-items: center; justify-content: space-between; gap: 1.5rem;
	padding: 0.75rem 1.5rem;
	border-radius: 1.25rem;
	border: 1px solid rgba(255,255,255,.7);
	background: rgba(255,255,255,.86);
	backdrop-filter: blur(20px) saturate(1.8);
	-webkit-backdrop-filter: blur(20px) saturate(1.8);
	box-shadow: 0 20px 45px -26px rgba(16,22,25,.32), inset 0 1px 0 rgba(255,255,255,.8);
	transition: background .45s var(--ease), border-color .45s var(--ease), box-shadow .45s var(--ease), padding .5s var(--ease);
}
/* Logo sizes, shared by the header and the footer so the two can't drift
   apart. --logo-h is the standard height: the footer's, and the header's
   once the bar docks. --logo-h-idle is the header's while the bar sits
   idle at the top of a page. Phones (up to 720px, where the header row is
   only the logo and the menu button) keep the original 2.25rem in both
   places and both states; tablets and desktops get the larger pair.
   --logo-ratio is the shape of the two logo files (420 x 70). */
:root { --logo-ratio: 6; --logo-h: 2.25rem; }
@media (min-width: 45.0625em) { /* 721px */ :root { --logo-h: 2.75rem; --logo-h-idle: 3.25rem; } }

/* flex-shrink: 0 -- .container is flex + justify-content: space-between
   with no explicit widths anywhere in the row, so when the row doesn't
   have enough space for everything at once, the default flex-shrink: 1
   let the browser claw space back from *whichever* item had room to give,
   logo included: measured shrinking it as far as 28px (from a natural
   ~216px) at some widths to keep the rest of the row from overflowing.
   The logo is the one thing in this row that must never compress -- it's
   the brand mark, not a control with room to spare -- so it's excluded
   from the browser's shrink pool entirely; the nav bar and header CTA
   (both meant to adapt) absorb tight widths instead. (From 721px the
   header lets it give back only what it gained; see below.) */
.site-branding { display: flex; align-items: center; flex-shrink: 0; }
.site-logo-link { display: flex; align-items: center; }
/* Sized by width, with the height following the files' own shape, so that
   wherever a container is narrower than the logo (the footer's brand
   column is about 266px at 981px, less with a scrollbar) the mark scales
   down whole. A fixed height would let the sitewide img { max-width: 100% }
   squash it sideways instead. aspect-ratio also holds the right shape
   before the image has loaded, when the HTML's width and height would
   otherwise set it. */
.site-logo-link img {
	width: calc(var(--logo-h) * var(--logo-ratio)); height: auto;
	aspect-ratio: var(--logo-ratio); display: block;
}
/* Header, tablets and desktops (721px up): the idle size while the bar sits
   at the top of a page; the standard size once it docks (.is-day) and the
   whole bar steps back. Phones keep everything above as it is.

   From 981px the full nav row is back and is tight at first. Rather than
   reserve a fixed guess at the nav's width, the logo is allowed to give
   back what it gained when the row runs short: the five nav labels and
   "Talk to us" can't shrink (single words, no-wrap), so the browser takes
   the shortfall from the logo alone, which scales down whole, never below
   the original 2.25rem (13.5rem wide). The nav gaining a label, or a
   desktop scrollbar taking 15px, is absorbed without any figure to update.

   The bar's height is set by its controls ("Talk to us"), never by the
   logo. .site-branding, the item the row actually measures, keeps the
   2.25rem box the logo used to fill, and the mark is centred on it,
   overhanging evenly into the pill's own padding. So the bar is exactly as
   tall as it was: in both states, and at every moment of the resize
   between them. Letting the logo size the row instead would make the bar
   about 3px taller while idle. The fixed box is on .site-branding rather
   than on the link so the link stays the true size of the mark: its focus
   ring and its click area are the logo you see. */
@media (min-width: 45.0625em) { /* 721px */
	.site-header:not(.is-day) { --logo-h: var(--logo-h-idle); }
	.site-header .site-branding { height: 2.25rem; flex-shrink: 1; min-width: calc(2.25rem * var(--logo-ratio)); }
	.site-header .site-logo-link,
	.site-header .logo-swap { min-width: 0; }
}
/* With the full nav in the row (981px up), the row's minimum gap is a
   touch wider than the nav's own 1.875rem between items, so when the row
   runs short and the logo gives way, it still reads as its own group
   rather than as the first word of the menu. Under space-between this
   minimum only bites on a tight row; anywhere roomier the spacing is
   exactly what it was. */
@media (min-width: 61.3125em) { /* 981px */
	.site-header .container { column-gap: 2rem; }
}

/* Two real logo files cross-fade on background, rather than filtering one —
   the color mark stays true on paper, the white mark stays true on ink. */
.logo-swap { position: relative; display: block; }
.logo-swap .logo-img--color { transition: opacity .35s ease; }
.logo-swap .logo-img--white { position: absolute; inset: 0; opacity: 0; transition: opacity .35s ease; }
/* From 721px up the header mark also changes size as the bar docks (see
   the logo-size block above), on the same .5s curve as the bar's own
   padding, so the two settle together. The logo is sized by width, so
   that is what moves; its height follows. */
@media (min-width: 45.0625em) { /* 721px */
	.site-header .logo-swap .logo-img { transition: opacity .35s ease, width .5s var(--ease); }
}

.primary-nav ul { display: flex; align-items: center; gap: 1.875rem; }
.primary-nav a { font-size: 0.90625rem; font-weight: 500; color: var(--ink-70); transition: color .2s ease; position: relative; }
.primary-nav a:hover, .primary-nav .current-menu-item > a, .primary-nav .current-menu-ancestor > a { color: var(--ink); }

/* Hover-triggered dropdowns for menu items with children */
.primary-nav .menu-item-has-children { position: relative; }
.primary-nav .menu-item-has-children > a { display: inline-flex; align-items: center; gap: 0.375rem; }
.primary-nav .menu-item-has-children > a::after {
	content: ''; width: 0.375rem; height: 0.375rem;
	border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor;
	transform: rotate(45deg); margin-top: -0.1875rem; transition: transform .15s ease;
}
.primary-nav .menu-item-has-children.is-open > a::after,
.primary-nav .menu-item-has-children:hover > a::after,
.primary-nav .menu-item-has-children:focus-within > a::after {
	transform: rotate(225deg); margin-top: 0.1875rem;
}
.primary-nav .sub-menu {
	display: block; position: absolute; top: 100%; left: 0; margin-top: 1rem; padding: 0.5rem;
	min-width: 14.375rem; z-index: 110;
	background: var(--paper-hi); border: 1px solid var(--hair-l); border-radius: 0.875rem;
	box-shadow: var(--shadow-soft);
	opacity: 0; visibility: hidden; transform: translateY(0.375rem);
	transition: opacity .15s ease, transform .15s ease, visibility .15s ease;
}
.primary-nav .sub-menu::before { content: ''; position: absolute; top: -1rem; left: 0; right: 0; height: 1rem; }
.primary-nav .menu-item-has-children.is-open > .sub-menu,
.primary-nav .menu-item-has-children:hover > .sub-menu,
.primary-nav .menu-item-has-children:focus-within > .sub-menu {
	opacity: 1; visibility: visible; transform: translateY(0);
}
.primary-nav .sub-menu li { width: 100%; }
.primary-nav .sub-menu a {
	display: block; padding: 0.5625rem 0.625rem; border-radius: 0.5625rem; font-size: 0.875rem;
	white-space: nowrap; color: var(--ink);
}
.primary-nav .sub-menu a:hover { background: var(--accent); }

.header-actions { display: flex; align-items: center; gap: 0.75rem; }
.header-actions .btn-primary { padding: 0.75rem 1.375rem; font-size: 0.90625rem; }

.menu-toggle { display: none; background: none; border: 1px solid var(--hair-l); border-radius: 0.5625rem; padding: 0.5rem 0.625rem; color: var(--ink); }
.menu-toggle svg { width: 1.25rem; height: 1.25rem; }
/* Hamburger-to-close morph -- the icon is already three independent <line>s
   (top/middle/bottom bars), which is exactly what this animation needs, so
   it's a pure CSS transform on each one rather than swapping in a second
   icon: no second SVG to keep in sync across every page, and it animates
   instead of just cutting between two states. Top and bottom bars rotate
   45°/-45° and slide the 5px (the bars' own spacing in the 24-unit viewBox)
   needed to meet at centre; the middle bar just fades. */
.menu-toggle svg line { transform-origin: center; transition: transform .25s var(--ease), opacity .15s ease; }
.site-header.is-open .menu-toggle svg line:nth-child(1) { transform: translateY(5px) rotate(45deg); }
.site-header.is-open .menu-toggle svg line:nth-child(2) { opacity: 0; }
.site-header.is-open .menu-toggle svg line:nth-child(3) { transform: translateY(-5px) rotate(-45deg); }

/* Homepage: nav starts fully transparent (no pill at all) + light-on-
   dark over the hero stage, then the glass pill above fades in once JS
   marks it .is-day (matching the scroll progress that contracts the hero).
   Pages with no dark hero at all show the pill from first paint — no dark
   hero to be transparent over, no JS dependency, nothing to cross into.
   Pages that open with one of the full-bleed dark heroes below get the
   same treatment via the :has() block further down. */
.home .site-header:not(.is-day) .container {
	background: transparent; border-color: transparent; backdrop-filter: none; box-shadow: none;
}
.home .site-header:not(.is-day) .logo-swap .logo-img--color { opacity: 0; }
.home .site-header:not(.is-day) .logo-swap .logo-img--white { opacity: 1; }
/* Light-on-dark is for the bar's own links only. Every link inside a
   dropdown panel (.sub-menu -- the Services mega-menu is one too) sits on
   the panel's solid white background whatever the bar is doing, so those
   links keep their own ink / hover / current colours. That is why these
   rules say :not(.sub-menu a) instead of undoing themselves for the panel
   afterwards: an undo rule only holds while it out-ranks these on
   specificity, and it stopped holding when :not(.is-open) (below) made
   these rank higher -- a hovered dropdown item then turned white on its
   own pale highlight.
   :not(.is-open) alongside :not(.is-day): once the hamburger panel is open
   on mobile, every link in it sits on that same solid white panel
   (.primary-nav background, below), not the transparent-over-dark-hero
   header. Without it this whole group still read the header as "not day
   yet" while the panel was open, painting every top-level label
   (Services, Products...) in near-white against the panel's white
   background -- legible only after scrolling past DOCK_AT flipped .is-day
   and this stopped applying. */
.home .site-header:not(.is-day):not(.is-open) .primary-nav a:not(.sub-menu a) { color: var(--lite-72); }
.home .site-header:not(.is-day):not(.is-open) .primary-nav a:hover:not(.sub-menu a),
.home .site-header:not(.is-day):not(.is-open) .primary-nav .current-menu-item > a:not(.sub-menu a) { color: #fff; }
.home .site-header:not(.is-day):not(.is-open) .primary-nav .sub-menu { background: var(--paper-hi); }
.home .site-header:not(.is-day) .menu-toggle { border-color: var(--hair-d); color: #EAF3F1; }

/* Every other dark-hero page (services hub, each service family, the
   photo-hero pages, agentic-ai's canvas-wrapped variant) gets the exact
   same transparent-until-scrolled treatment as the homepage above, just
   driven by plain scroll position (initHeroHeader() in main.js) instead
   of the homepage's shrinking #heroStage — these pages have no stage to
   shrink. Scoped to this explicit hero list via :has() rather than a
   generic [class*="-hero"] match, so unaudited "-hero" pages (blog posts,
   case studies, the product page) keep the pill always solid — it still
   gets the plain size shrink on scroll from the generic .is-day rule
   above, just none of this color/transparency treatment, since there's
   no dark hero underneath it to have been transparent over.
   These heroes also already reserve their own room for the fixed header
   in their top padding (see .ainative-hero etc. below), so #primary's
   generic 84px header-clearance padding has to get out of the way here
   too, or the header's height gets double-counted as dead space above
   the hero. */
#primary:has(> :is(.services-hero, .ainative-hero, .cloud-hero, .qa-hero, .bi-hero, .teams-hero, .genai-hero, .photo-hero, .agentic-atmosphere, .bimig-hero)) {
	padding-top: 0;
}
.site-header:has(~ #primary > :is(.services-hero, .ainative-hero, .cloud-hero, .qa-hero, .bi-hero, .teams-hero, .genai-hero, .photo-hero, .agentic-atmosphere, .bimig-hero)):not(.is-day) .container {
	background: transparent; border-color: transparent; backdrop-filter: none; box-shadow: none;
}
.site-header:has(~ #primary > :is(.services-hero, .ainative-hero, .cloud-hero, .qa-hero, .bi-hero, .teams-hero, .genai-hero, .photo-hero, .agentic-atmosphere, .bimig-hero)):not(.is-day) .logo-swap .logo-img--color { opacity: 0; }
.site-header:has(~ #primary > :is(.services-hero, .ainative-hero, .cloud-hero, .qa-hero, .bi-hero, .teams-hero, .genai-hero, .photo-hero, .agentic-atmosphere, .bimig-hero)):not(.is-day) .logo-swap .logo-img--white { opacity: 1; }
/* Same scoping as the homepage's equivalent rules above: light-on-dark for
   the bar's own links only (:not(.sub-menu a)), and :not(.is-open) so the
   solid-white mobile panel never gets it. */
.site-header:has(~ #primary > :is(.services-hero, .ainative-hero, .cloud-hero, .qa-hero, .bi-hero, .teams-hero, .genai-hero, .photo-hero, .agentic-atmosphere, .bimig-hero)):not(.is-day):not(.is-open) .primary-nav a:not(.sub-menu a) { color: var(--lite-72); }
.site-header:has(~ #primary > :is(.services-hero, .ainative-hero, .cloud-hero, .qa-hero, .bi-hero, .teams-hero, .genai-hero, .photo-hero, .agentic-atmosphere, .bimig-hero)):not(.is-day):not(.is-open) .primary-nav a:hover:not(.sub-menu a),
.site-header:has(~ #primary > :is(.services-hero, .ainative-hero, .cloud-hero, .qa-hero, .bi-hero, .teams-hero, .genai-hero, .photo-hero, .agentic-atmosphere, .bimig-hero)):not(.is-day):not(.is-open) .primary-nav .current-menu-item > a:not(.sub-menu a) { color: #fff; }
.site-header:has(~ #primary > :is(.services-hero, .ainative-hero, .cloud-hero, .qa-hero, .bi-hero, .teams-hero, .genai-hero, .photo-hero, .agentic-atmosphere, .bimig-hero)):not(.is-day):not(.is-open) .primary-nav .sub-menu { background: var(--paper-hi); }
.site-header:has(~ #primary > :is(.services-hero, .ainative-hero, .cloud-hero, .qa-hero, .bi-hero, .teams-hero, .genai-hero, .photo-hero, .agentic-atmosphere, .bimig-hero)):not(.is-day) .menu-toggle { border-color: var(--hair-d); color: #EAF3F1; }
/* No matching .is-day padding-shrink pair here — the generic
   .site-header.is-day rule near the top of this file already covers
   every page, this hero list included. */

/* ==========================================================================
   Dark section scoping
   Redeclaring the semantic tokens locally flips every nested component
   (.card, .btn-outline, .pill, .eyebrow, tags…) to the dark palette for
   free — no per-component dark variants needed.
   ========================================================================== */
.hero, .section--dark, .site-footer {
	--background: var(--ink);
	--foreground: #EAF3F1;
	--surface: var(--ink);
	--card: var(--ink-raise);
	--popover: rgba(255,255,255,.05);
	--muted: rgba(255,255,255,.07);
	--muted-foreground: var(--lite-72);
	--accent: rgba(43,52,224,.16);
	--accent-foreground: #B9C0FF;
	--border: rgba(234,243,241,.14);
	--ring: var(--beam);
	background: var(--background);
	color: var(--foreground);
}
.section--dark .eyebrow { color: var(--butter); }

/* Cobalt is the accent on paper, but at 2.3:1 on --ink it is under the
   floor for body-size text. Dark sections take the brand teal (5.4:1).
   The matching .card-link override lives with the card rules below, where
   it can outrank the existing cobalt one on source order. */
.section--dark .case-tag { color: var(--teal); }

/* --------------------------------------------------------------------------
   Stacked-card tones. The rule is that dark carries proof: a card asking
   the visitor to believe something (track record, client outcomes, global
   reach) goes dark, and a card that explains stays in daylight. It also
   guarantees no two adjacent cards share a surface, which is what makes
   the stacked edge visible at all — nine white cards stack invisibly.

   Mist is a tinted daylight; the cards inside it stay paper-white so the
   grid still lifts off the section.
   -------------------------------------------------------------------------- */
.section--mist { --background: #E7F0EF; --surface: #E7F0EF; }
/* Opt-in (body.ind-tonal): paint .section--mist as a teal-tinted band (paper + ~15% teal) so alternating sections separate; opt-in because 11 pages use the class unpainted. Eyebrows move to --teal-ink, since --teal-deep drops to ~3.6:1 on the band. */
body.ind-tonal { --band: #D8E6E7; }
body.ind-tonal .section--mist { --background: var(--band); --surface: var(--band); background: var(--band); }
body.ind-tonal .section--mist .eyebrow { color: var(--teal-ink); }

/* The last card closes on the hero's own ink, so the page ends in the
   colour it opened in before handing off to the CTA band and footer. */
.section--dark.section--night {
	--background: var(--hero-ink);
	--surface: var(--hero-ink);
	--card: var(--hero-ink-raise);
}

/* The hero alone gets a background pulled from the logo's own teal rather
   than the neutral slate the other dark sections share. Its ground is
   --home-hero-ink, a step lighter than the --hero-ink the service-page
   heroes and the closing band use. The cards inside it (router panel, code
   card, approval card, the outline button) deliberately stay on
   --hero-ink-raise, which now sits a shade darker than the ground rather
   than lighter. */
.hero {
	--background: var(--home-hero-ink);
	--surface: var(--home-hero-ink);
	--card: var(--hero-ink-raise);
}

/* ==========================================================================
   Hero stage — the dark hero contracts into a floating card, revealing the
   daylight page beneath it. Sticky stage per the reference implementation;
   overflow-x on html/body must stay `clip`, not `hidden`, or this breaks.

   The contraction is horizontal-only: clip-path insets the left/right edges
   as --pe advances (height stays 100dvh throughout, never scaled), so the
   hero narrows into a rounded card without ever shrinking vertically — a
   plain transform:scale would have shrunk it from all four sides at once.
   The stage's own height is what sets how much scroll distance that runway
   gets; kept short on purpose so the page keeps moving as soon as you start
   scrolling, rather than pinning the hero for a long, stationary hold.
   Shortened again alongside --shrink-x, in proportion, so the hold stays
   sized to what it's actually showing — a bigger hold spent on a smaller
   visual change would read as the scroll stalling for nothing. */
.hero-stage { position: relative; height: 113vh; background: var(--paper); }
.hero-stage__sticky {
	position: sticky; top: 0; height: 100vh; height: 100dvh;
	display: grid; place-items: center; overflow: clip;
}
/* Sits behind .hero, sized to the sticky stage itself rather than the
   card — as .hero's clip-path narrows the card inward, this fixed-size
   grid is what gets uncovered at the margins, instead of the plain
   --paper backdrop. It never scales or shifts with the contraction,
   matching the "card shrinks, backdrop doesn't" read from the reference. */
.hero-stage-grid {
	position: absolute; inset: 0; opacity: .6; pointer-events: none;
	background-image:
		linear-gradient(rgba(16,22,25,.06) 1px, transparent 1px),
		linear-gradient(90deg, rgba(16,22,25,.06) 1px, transparent 1px);
	background-size: 2.5rem 2.5rem;
	-webkit-mask-image: radial-gradient(120% 90% at 50% 50%, #000 30%, transparent 82%);
	        mask-image: radial-gradient(120% 90% at 50% 50%, #000 30%, transparent 82%);
}
.hero {
	position: relative; width: 100%; height: 100vh; height: 100dvh; overflow: hidden;
	clip-path: inset(0 calc(var(--shrink-x) * var(--pe) * 50%) 0 calc(var(--shrink-x) * var(--pe) * 50%) round calc(3.25rem * var(--pe)));
	border-radius: calc(3.25rem * var(--pe));
	box-shadow: 0 40px 100px -50px rgba(16,22,25, calc(.85 * var(--pe)));
	will-change: clip-path;
}
.hero::before {
	content: ''; position: absolute; inset: 0;
	background: var(--gradient-hero); opacity: calc(.9 - .2 * var(--pe)); pointer-events: none; filter: blur(32px);
	background-size: 130% 130%; animation: aurora-drift 18s ease-in-out infinite;
}
@keyframes aurora-drift {
	0%, 100% { background-position: 0% 0%; }
	50% { background-position: 100% 40%; }
}
.hero-grid-bg {
	position: absolute; inset: 0; opacity: .5; pointer-events: none;
	background-image:
		linear-gradient(rgba(234,243,241,.07) 1px, transparent 1px),
		linear-gradient(90deg, rgba(234,243,241,.07) 1px, transparent 1px);
	background-size: 2.5rem 2.5rem;
	-webkit-mask-image: radial-gradient(120% 90% at 50% 30%, #000 20%, transparent 78%);
	        mask-image: radial-gradient(120% 90% at 50% 30%, #000 20%, transparent 78%);
}
.hero .container {
	position: relative; z-index: 1; width: 100%;
	display: grid; grid-template-columns: 1.05fr 0.95fr; gap: 3.5rem; align-items: center;
	padding-top: 6.75rem; padding-bottom: 3rem;
	transform: translateY(calc(-1px * var(--reveal, 0)));
	will-change: transform;
}
.hero-copy .eyebrow {
	/* Plain --teal reads clearly enough on paper, but gets lost against
	   the busy dark aurora here. --paper matches it to the headline's own
	   colour instead, which flattens the hierarchy between label and H1.
	   This keeps the teal hue (still reads as "label", not "heading") but
	   lifts it toward white for the contrast the dark hero needs. */
	color: color-mix(in srgb, var(--teal) 60%, white);
	padding: 0.5rem 1.125rem 0.5rem 0.875rem;
	border-radius: 999px;
	border: 1px solid rgba(255,255,255,.14);
	background: rgba(255,255,255,.06);
	backdrop-filter: blur(16px) saturate(1.4);
	-webkit-backdrop-filter: blur(16px) saturate(1.4);
	margin-bottom: 1.5rem;
}
.hero-copy .eyebrow::before {
	width: 0.375rem; height: 0.375rem; border-radius: 50%;
	background: linear-gradient(135deg, var(--teal), var(--beam) 55%, var(--sun));
	opacity: 1;
}
/* Capped at 3rem (was 3.85rem) so the hero-copy column is short enough on
   common desktop/laptop heights that .hero-stats lands inside the initial
   100dvh viewport — visible on first load instead of needing a scroll-driven
   lift to bring it into view (see .hero .container's --reveal, below). */
.hero-headline { font-size: clamp(2.35rem, 4.6vw, 3rem); }
.hero-headline .ln { display: block; overflow: hidden; }
.hero-headline .ln > span { display: block; transform: translateY(105%); animation: rise .9s var(--ease) forwards; }
.hero-headline .ln:nth-child(2) > span { animation-delay: .09s; }
@keyframes rise { to { transform: translateY(0); } }
.hero-lede { margin-top: 1.5rem; font-size: 1.15rem; color: var(--muted-foreground); max-width: 35rem; }
.hero-cta { display: flex; gap: 0.875rem; margin-top: 2.25rem; flex-wrap: wrap; }
.hero-stats { display: grid; grid-template-columns: repeat(4, auto); gap: 2rem; margin-top: 3.25rem; }

/* Homepage only: the copy column (eyebrow + headline + lede + cta + stats)
   is taller than the 100dvh hero it's clipped to once the viewport gets
   short — an 11" laptop (~640px tall) crops the stats row, the page's key
   trust signal, before the visitor scrolls into the hero-stage's reveal
   runway far enough to see it. Rather than pick one breakpoint, the top
   padding and four internal gaps shrink together on a fluid scale tied
   to viewport height: reaching their full size at ~800px+ (a typical
   13"-16" laptop, matching the assumption the headline was already sized
   for), compressing smoothly toward a floor at 640px — so nothing snaps
   or looks cramped at any height in between. Floor values have been
   tuned more than once (each time funding a wider nav-to-eyebrow gap,
   the one visitors actually notice, from elsewhere in the column) —
   only the current numbers matter below, not the history.

   Latest change: cutting the lede's trailing clause ("...that helps
   organizations innovate faster") dropped it from four lines to three
   at this column's 560px max-width — a fixed ~30px freed at every
   viewport height, not just short ones. At the 640px floor that funded,
   together with trimming the eyebrow's own margin a little further,
   another jump in the nav-to-eyebrow gap (108px -> 135px) plus giving
   a few of those px back to the CTA-to-stats gap (26px -> 32px, still
   short of its original 36px). At the 800px+ ceiling, where the nav
   gap and eyebrow margin already had enough room and are left exactly
   as they were, the freed line goes entirely to the CTA and stats
   margins instead — split evenly, as extra breathing room now that
   there's nothing forcing them to stay put (36px -> 51px CTA,
   76px -> 91px stats). */
/* --copy-shift: the larger header logo (721px up; see the logo sizes near
   the top of this file) sits 0.5rem lower, into the space above the
   eyebrow. The copy moves down a quarter-rem to give some of that back,
   and the same amount comes off the gap above the stats, so the stats row
   stays exactly where it was and the column is exactly as tall as before,
   which the fluid fit above depends on. Phones keep the old logo, so
   nothing moves there. */
.home .hero { --copy-shift: 0rem; }
@media (min-width: 45.0625em) { /* 721px */ .home .hero { --copy-shift: 0.25rem; } }
.home .hero .container { padding-top: calc(clamp(8.4375rem, 21.875vh - 0.3125rem, 10.625rem) + var(--copy-shift)); }
.home .hero-copy .eyebrow { margin-bottom: clamp(0.625rem, 8.75vh - 2.875rem, 1.5rem); }
.home .hero-lede { margin-top: clamp(1.0625rem, 4.375vh - 0.6875rem, 1.5rem); }
.home .hero-cta { margin-top: clamp(1.5625rem, 16.25vh - 4.9375rem, 3.1875rem); }
.home .hero-stats { margin-top: calc(clamp(2rem, 36.875vh - 12.75rem, 5.6875rem) - var(--copy-shift)); }
.hero-stats div strong { display: block; font-family: var(--font-display); font-variation-settings: 'wdth' 108, 'wght' 700; font-size: 1.6rem; font-variant-numeric: tabular-nums; }
.hero-stats div span { font-size: var(--text-min-sans); color: var(--muted-foreground); }

/* align-items: center on .hero .container centers the visual against the
   taller copy column, but dead center left too much air above the panel
   under the nav. `top` (not transform, not margin-top) shifts it up the
   full 67px (~0.7in): margin-top only moved it half that distance, since
   align-items: center re-centers the taller margin box and splits the
   difference above/below -- and .hero-visual also picks up the .reveal
   entrance animation (main.js), whose .is-visible state sets its own
   transform: translateY(0) at higher specificity, silently cancelling any
   transform set here once the fade-in settles. */
.hero-visual { position: relative; top: -4.1875rem; --hero-panel-pad: 1.25rem; }
.hero-panel {
	background: var(--card); border: 1px solid var(--border); border-radius: var(--radius);
	box-shadow: var(--shadow-soft); padding: var(--hero-panel-pad);
}
.hero-panel-dots { display: flex; gap: 0.375rem; margin-bottom: 1rem; }
.hero-panel-dots span { width: 0.625rem; height: 0.625rem; border-radius: 50%; }
.hero-panel-dots span:nth-child(1) { background: var(--sun); }
.hero-panel-dots span:nth-child(2) { background: var(--butter); }
.hero-panel-dots span:nth-child(3) { background: var(--teal); }
/* Hero router — one batch of work fanned across the models it gets routed
   to. Replaces the old Requests/s + Uptime tiles (hosting metrics for
   infrastructure DivIni doesn't run for the visitor) and the empty
   .hero-panel-chart placeholder.

   Drawn as a fan rather than share-of-volume bars on purpose: the lead
   practice card further down the page is a horizontal-bar waterfall, and
   two bar charts would read as a house texture instead of two arguments.
   The on-prem route is the only warm mark on the panel — it carries the
   sensitivity axis (and the regulated-sector claim) rather than the cost
   one, so it should not look like a fourth escalation tier. */
.hero-router { font-family: var(--font-mono); }
.router-head {
	display: flex; align-items: baseline; justify-content: space-between; gap: 0.75rem;
	padding-bottom: 0.8125rem; margin-bottom: 0.9375rem; border-bottom: 1px solid var(--border);
}
.router-task { font-size: 0.8125rem; color: var(--foreground); }
.router-label {
	font-size: var(--text-min); letter-spacing: .16em; text-transform: uppercase;
	color: rgba(234,243,241,.38); flex: none;
}

.router-body { display: grid; grid-template-columns: auto minmax(2.75rem, 1fr) minmax(0, 2.3fr); align-items: center; }
.router-source { text-align: right; padding-right: 0.25rem; }
.router-source strong {
	display: block; font-family: var(--font-display);
	font-variation-settings: 'wdth' 106, 'wght' 700; font-size: 1.35rem;
	font-variant-numeric: tabular-nums; line-height: 1.1;
}
.router-source span { font-size: var(--text-min); color: rgba(234,243,241,.42); }

/* preserveAspectRatio="none" stretches the 100x152 viewBox to whatever the
   middle column ends up being, so every stroke needs non-scaling-stroke or
   the curves thin out horizontally as the column grows. */
.router-fan { width: 100%; height: 9.5rem; display: block; overflow: visible; }
.router-fan path { fill: none; stroke: var(--teal); stroke-linecap: round; }
.router-fan .fan-1 { stroke-width: 2;    opacity: .95; }
.router-fan .fan-2 { stroke-width: 1.5;  opacity: .6; }
.router-fan .fan-3 { stroke-width: 1.15; opacity: .4; }
.router-fan .fan-4 { stroke-width: 1.5;  opacity: .85; stroke: var(--sun); }

.router-models { list-style: none; margin: 0; padding: 0; height: 9.5rem; display: grid; grid-template-rows: repeat(4, 1fr); }
.router-models li {
	display: grid; grid-template-columns: 1fr auto; align-items: center;
	column-gap: 0.625rem; padding-left: 0.75rem; border-left: 1px solid var(--border);
	/* Two lines per row (name, reason) have to fit a quarter of the 9.5rem
	   track the fan is drawn to; at the body's 1.6 they no longer do once
	   the reason is at --text-min, and the last row spills out of the box. */
	line-height: 1.5;
}
.rm-name { font-size: 0.75rem; color: var(--foreground); }
.rm-why { grid-column: 1; font-size: var(--text-min); color: rgba(234,243,241,.42); }
.router-models b {
	grid-row: 1 / -1; grid-column: 2; font-size: 0.8125rem; font-weight: 400;
	font-variant-numeric: tabular-nums; color: var(--teal);
}
.rm--onprem .rm-name, .rm--onprem b { color: var(--sun); }

.router-foot { margin-top: 0.9375rem; text-align: right; font-size: var(--text-min); color: rgba(234,243,241,.42); }

/* Below ~560px the models column drops under ~155px, where the reason
   labels and the "· on-prem" qualifier stop sharing a line with their
   counts and the rows wrap out of their tracks. Both get cut: the fan, the
   names and the counts still carry the routing idea, and the on-prem route
   keeps its persimmon and its callout in the cost line below. */
@media (max-width: 35em) { /* 560px */
	.rm-why, .rm-tag { display: none; }
	/* preserveAspectRatio="none" restretches the viewBox to the new height,
	   so the endpoints stay on the row centres without touching the paths. */
	.router-fan, .router-models { height: 8rem; }
	.router-task { font-size: 0.75rem; }
	.router-source strong { font-size: 1.15rem; }
}

/* The fan draws itself once, which is what makes "routing" read as an act
   rather than a static list. Dash length is deliberately over-long — the
   stretched curves run past 200 user units, and a short dasharray leaves a
   visible gap where the pattern repeats. */
@media (prefers-reduced-motion: no-preference) {
	.router-fan path { stroke-dasharray: 260; stroke-dashoffset: 260; animation: fan-draw .75s var(--ease) forwards; }
	.router-fan .fan-1 { animation-delay: .35s; }
	.router-fan .fan-2 { animation-delay: .45s; }
	.router-fan .fan-3 { animation-delay: .55s; }
	.router-fan .fan-4 { animation-delay: .65s; }
	@keyframes fan-draw { to { stroke-dashoffset: 0; } }
}

/* Hangs below the panel rather than across it, overlapping only the panel's
   bottom padding: enough for the layering to read, never over anything that
   has to be legible. Anchored by its own top edge rather than the old
   bottom: -118px, so the overlap stays exactly the padding whatever the
   card's height or the text size: measured, that offset had drifted from
   the intended 23px overlap to 62px once a code line wrapped, and the card
   sat on the router's last row (and, at ~1100px wide, on its cost line).
   19.25rem is just wide enough to keep every code line on one line. */
.hero-code {
	position: absolute; left: -1.125rem; top: calc(100% - var(--hero-panel-pad)); width: 19.25rem;
	background: var(--card); border: 1px solid var(--border); border-radius: 0.75rem;
	padding: 1rem; font-family: var(--font-mono); font-size: 0.75rem; color: var(--teal);
	box-shadow: var(--shadow-soft);
}
.hero-code .file { color: var(--muted-foreground); margin-bottom: 0.5rem; display: flex; align-items: center; gap: 0.375rem; }

/* Third floating card, right side. Deliberately previews the exact run the
   AI & Agentic AI lead card unpacks further down the page (run 4a2f: plan →
   claims.search → human approval → deploy) — the hero teases the same
   checkpoint the services grid later proves in full, instead of inventing
   a third unrelated demo. The panel's footer text runs along its bottom
   edge on the right, so this card hangs clear below the panel entirely,
   a fixed step under it, rather than overlapping it. Anchored by its top
   edge for the same reason as .hero-code (a bottom offset rises into the
   panel whenever the card's own text grows), and the step keeps the two
   cards staggered, the left one higher. 236px keeps "Deploy claims-router
   v1.4" on one line -- at 214px it wrapped to two, which pushed the card
   low enough to risk clipping against .hero's own overflow:hidden on short
   viewports. */
.hero-approval {
	position: absolute; right: -1rem; top: calc(100% + 0.5rem); width: 14.75rem;
	background: var(--card); border: 1px solid var(--border); border-radius: 0.75rem;
	padding: 0.9375rem 1rem; font-family: var(--font-mono); box-shadow: var(--shadow-soft);
}
.approval-head { display: flex; align-items: center; gap: 0.4375rem; margin-bottom: 0.6875rem; }
.approval-dot { width: 0.4375rem; height: 0.4375rem; border-radius: 50%; background: #7BE0A8; box-shadow: 0 0 0 3px rgba(123,224,168,.18); flex: none; }
.approval-run { font-size: var(--text-min); color: var(--muted-foreground); }
.approval-label {
	margin-left: auto; font-size: var(--text-min); letter-spacing: .14em; text-transform: uppercase;
	color: rgba(234,243,241,.38);
}
.approval-task { font-size: 0.78125rem; color: var(--foreground); margin-bottom: 0.8125rem; line-height: 1.4; }
/* Stacked rather than space-between: "M. Ortiz approved" plus "3m 20s hold"
   side by side overruns a 214px card in monospace at any reasonable size,
   which is what was spilling text past the border. Each line fits on its
   own with room to spare. */
.approval-foot { padding-top: 0.75rem; border-top: 1px solid var(--border); display: grid; gap: 0.375rem; }
.approval-who { display: flex; align-items: center; gap: 0.3125rem; font-size: var(--text-min); color: #7BE0A8; white-space: nowrap; }
.approval-who svg { flex: none; }
.approval-time { font-size: var(--text-min); color: var(--butter); font-variant-numeric: tabular-nums; white-space: nowrap; }

@media (prefers-reduced-motion: no-preference) {
	.approval-dot { animation: approval-pulse 2.4s ease-in-out infinite; }
	@keyframes approval-pulse {
		0%, 100% { box-shadow: 0 0 0 3px rgba(123,224,168,.18); }
		50% { box-shadow: 0 0 0 6px rgba(123,224,168,.04); }
	}
}

/* split layout — reused by the global-presence section (copy + visual),
   independent of .hero so it never inherits the stage transform */
.split { display: grid; grid-template-columns: 1.05fr 0.95fr; gap: 3.5rem; align-items: center; }

/* ==========================================================================
   Card-stacking scroll — each pinned item slides under the next one as you
   scroll past it, echoing the hero's own contraction. Used only by the
   testimonials section: the nine homepage sections that used to stack this
   way were unwrapped back to a normal flow (a full-viewport pin-and-cover
   per section, nine times down one page, was too much scroll spent on a
   section you only read once) — three short testimonial cards read here,
   is what the effect is actually suited to.
   ========================================================================== */
/* .stack is display:contents so its children (.stack-item) sit as ordinary
   siblings sharing a containing block — one sticky child per wrapper can't
   produce a cover on its own, since the next card lays out after the
   pinned one and never climbs past its bottom edge otherwise. That's what
   lets a higher-z card scroll up over the one still pinned beneath it. */
.stack { display: contents; }
/* The margin below each card IS the runway: it holds a pinned card alone on
   screen before the next one arrives. */
.stack > .stack-item {
	margin: 0 auto clamp(3rem, 9vh, 6.25rem);
	max-width: 40rem;
	/* --stick-top is measured per card in JS: cards shorter than the
	   viewport pin at 96px, taller ones get a negative value so they pin by
	   their bottom edge instead. Falls back to 96px with JS off, where the
	   no-js rule below drops sticky entirely anyway. */
	position: sticky; top: var(--stick-top, 6rem);
	z-index: calc(20 + var(--i, 0));
	/* !important: .testimonial-card is also in initReveal's selector list
	   (for its fade-in-on-scroll everywhere else it's used, e.g. the
	   about-us carousel), which sets its own `transform: translateY(...)`
	   at the same 2-class specificity as this rule and wins on source
	   order once .is-visible lands — silently cancelling the stack scale
	   the same way the .hero-visual comment above describes. Winning that
	   fight on purpose costs the slide-up entrance (opacity fade still
	   plays); the alternative is two competing scroll-driven transforms. */
	transform: scale(calc(1 - .06 * var(--stack-p, 0))) !important;
	filter: brightness(calc(1 - .20 * var(--stack-p, 0))) !important;
	transition: transform .12s linear, filter .12s linear;
}
/* A pinning, dimming stack is precisely what this preference is asking us
   not to do — and initStackCards bails early here, so --stick-top would
   never be measured and a taller card would stall against the 96px
   fallback. The cards just flow as a plain list instead. */
@media (prefers-reduced-motion: reduce) {
	.stack > .stack-item { position: static; transform: none !important; filter: none !important; margin-bottom: 1.5rem; }
}

/* ==========================================================================
   Cards grid (services / features)
   ========================================================================== */
.grid { display: grid; gap: 1.375rem; }
.grid-2 { grid-template-columns: repeat(2, 1fr); }
.grid-3 { grid-template-columns: repeat(3, 1fr); }
.grid-4 { grid-template-columns: repeat(4, 1fr); }

.card {
	display: block; color: inherit; text-decoration: none;
	background: var(--card); border: 1px solid var(--border); border-radius: var(--radius);
	padding: 1.875rem; transition: border-color .2s ease, transform .35s var(--ease), box-shadow .35s;
	position: relative; overflow: hidden;
}
.card:hover { border-color: var(--teal); transform: translateY(-0.375rem) !important; box-shadow: var(--shadow-soft); }
.card-icon {
	width: 2.875rem; height: 2.875rem; border-radius: 0.75rem; display: flex; align-items: center; justify-content: center;
	background: var(--accent); color: var(--teal-deep); margin-bottom: 1.25rem;
}
.section--dark .card-icon { color: var(--beam); background: rgba(43,52,224,.18); }
.card-icon svg { width: 1.3125rem; height: 1.3125rem; }
.card h3 { font-size: 1.1rem; margin-bottom: 0.625rem; font-variation-settings: 'wdth' 110, 'wght' 680; }
.card p { color: var(--muted-foreground); font-size: .94rem; }
.card-link { display: inline-flex; align-items: center; gap: 0.375rem; margin-top: 1rem; font-size: .88rem; font-weight: 600; color: var(--teal-deep); }
/* Teal, not cobalt: --beam is 2.3:1 on --ink, under the floor for text at
   this size. Now that Featured work is a dark card this carries three
   "Read case study" links. */
.section--dark .card-link { color: var(--teal); }
.card-link svg { transition: transform .3s var(--ease); }
.card:hover .card-link svg { transform: translateX(0.25rem); }

/* Small in-card tag list — same visual recipe as .tech-row .tags span and
   .data-foundation .tags span (999px pill, hairline border, .85rem), but
   filled with --muted rather than --card/--popover: these sit inside
   .card, whose own background already is --card, so a --card-filled pill
   would be invisible against it. */
.card-tags-label {
	display: block; font-family: var(--font-mono); font-size: var(--text-min); font-weight: 600;
	letter-spacing: .08em; text-transform: uppercase; color: var(--muted-foreground);
	margin: 1rem 0 0.5rem;
}
.card-tags { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 0.375rem; }
.card-tags li { padding: 0.3125rem 0.6875rem; border-radius: 999px; background: var(--muted); border: 1px solid var(--border); font-size: var(--text-min-sans); color: var(--muted-foreground); }
/* A card that carries tags stacks as a column so its tag block sits on the
   bottom edge, level across a row, instead of trailing each card's own
   paragraph at whatever height that happens to end. Only the first label
   takes the auto margin: a card with two groups (Web / Mobile) keeps them
   together at the bottom rather than splitting the free space between them.
   The padding is the 16px gap the label's own margin no longer guarantees
   once that margin is auto. */
.card:has(> .card-tags) { display: flex; flex-direction: column; }
.card > p + .card-tags-label { margin-top: auto; padding-top: 1rem; }

/* Detail row — an alternating, full-width heading + lead + checklist block
   for a handful of substantial topics that have no per-topic photography.
   Same problem .qa-topic solved before Quality Engineering had real images
   to put in it (see "why text-only" in
   docs/design/quality-assurance-family-design.md): a .card grid squeezes a
   real paragraph into a ~300px column, but inventing stock photography per
   topic isn't the honest fix either. Same proportions as .qa-topic (split
   grid, tinted panel, checklist bullets), with the media slot holding one
   large icon instead of a sourced photo. --detail-tint/--detail-accent
   stay a single pair across every row on a given page — unlike .qa-topic's
   per-photo palette, there's no real photo here to derive a different one
   from — so .reverse alone carries the zigzag rhythm. */
.detail-row { display: grid; grid-template-columns: minmax(0, .82fr) minmax(0, 1.18fr); align-items: stretch; border-radius: var(--radius-lg); overflow: hidden; background: var(--detail-tint); }
.detail-row + .detail-row { margin-top: 1.75rem; }
.detail-row.reverse .detail-media { order: 2; }
.detail-row.reverse .detail-copy { order: 1; }
.detail-media { display: flex; align-items: center; justify-content: center; min-height: 13.75rem; padding: 2rem; }
/* Each icon is a small drawing of what its row describes (a calendar of
   control checks, a risk heat map, a board agenda...), drawn on a 120-unit
   grid with a 3-unit line, so it needs this size to read; at the old 72px
   the detail turned to mush and the panel sat mostly empty. */
.detail-media svg { width: 9rem; height: 9rem; color: var(--detail-accent); }
.detail-copy { padding: 2.5rem 2.75rem; display: flex; flex-direction: column; justify-content: center; }
.detail-copy::before { content: ''; display: block; width: 2.125rem; height: 0.1875rem; border-radius: 2px; background: var(--detail-accent); margin-bottom: 1rem; }
.detail-copy h3 { font-family: var(--font-display); font-size: 1.4rem; margin: 0 0 0.75rem; color: var(--ink); }
.detail-lead { font-size: 1rem; line-height: 1.65; color: var(--ink-70); margin: 0 0 1.125rem; }
.detail-check { margin: 0; padding: 0; }
.detail-check > li { position: relative; padding-left: 1.5rem; margin-bottom: 0.5625rem; line-height: 1.6; list-style: none; color: var(--ink-70); }
.detail-check > li::before { content: ''; position: absolute; left: 1px; top: .55em; width: 0.4375rem; height: 0.25rem; border-left: 1.75px solid var(--detail-accent); border-bottom: 1.75px solid var(--detail-accent); transform: rotate(-45deg); }
@media (max-width: 53.75em) { /* 860px */
	.detail-copy { padding: 2rem 1.875rem; }
}
@media (max-width: 45em) { /* 720px */
	.detail-row, .detail-row.reverse { grid-template-columns: 1fr; }
	.detail-row .detail-media, .detail-row.reverse .detail-media { order: 1; min-height: 8.75rem; padding: 1.5rem; }
	.detail-row .detail-copy, .detail-row.reverse .detail-copy { order: 2; }
	/* Smaller than desktop so the stacked panel keeps its 140px height. */
	.detail-media svg { width: 5rem; height: 5rem; }
}

/* Centered "see more" link under a grid, e.g. Featured work's link through
   to the full case-studies archive. Reuses .card-link's look rather than
   inventing a new link style. */
.section-more { text-align: center; margin-top: 2.5rem; }
.section-more .card-link { margin-top: 0; }
.section-more .card-link:hover svg { transform: translateX(0.25rem); }

/* ==========================================================================
   Services — eight practices, equal weight. No "lead practice" tier: every
   card below shares the same icon/heading/body type scale (set once, at
   the bottom of this block, for every .services-grid card). The two AI
   cards keep the dark instrument-panel surface and run wider, because
   their visuals (a run waterfall, a RAG lookup) are landscape-format and
   need the width — not because they outrank the other six. The other six
   run in a 3-up grid with more vertical room instead, so width becomes a
   difference in shape, not in importance.

   The rows are the four mega-menu families in order — Applied AI (2),
   Engineering (3, Business Intelligence closing it out), Quality Assurance
   + Teams sharing the last row (3) — with no family label needed: the 2-3-3
   shape of the grid carries the grouping on its own.
   ========================================================================== */
.services-grid { display: flex; flex-direction: column; gap: 1.375rem; }
.services-lead-row { display: grid; grid-template-columns: 1fr 1fr; gap: 1.375rem; }
.services-reg-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.375rem; }
.services-grid .card--lead {
	display: grid;
	grid-template-columns: 1fr;
	gap: 0.625rem;
	align-items: center;
	padding: 1.0625rem;
	--background: var(--ink);
	--foreground: #EAF3F1;
	--surface: var(--ink);
	--card: var(--ink-raise);
	--popover: rgba(255,255,255,.05);
	--muted: rgba(255,255,255,.07);
	--muted-foreground: var(--lite-72);
	--accent: rgba(93,160,172,.16);
	--accent-foreground: var(--teal);
	--border: rgba(234,243,241,.14);
	background: var(--background);
	color: var(--foreground);
}
.services-grid .card--lead:hover { border-color: var(--beam); }
/* Spacing only, never font-size — the two wide cards still share the exact
   type scale every other card uses (see the shared rules further down);
   they just carry it in less vertical padding, which is how their area
   comes down toward the six narrower cards' without shrinking the type
   that's the actual argument for "no card outranks another". */
/* Icon and CTA colour matched to the six practice cards below — teal is
   the one consistent identity colour across all eight now, icon and link
   alike, rather than each lead card carrying its own cobalt accent. */
.card--lead .card-icon { margin-bottom: 0.625rem; background: var(--accent); color: var(--teal); }
.card--lead h3 { margin-bottom: 0.3125rem; }
/* The CTA sits after the visual panel, not inside the copy block above it
   — same bottom-anchored position as the six practice cards, so "what do I
   click next" is answered in the same place on every card in this grid.
   No horizontal margin needed: it's a direct grid child of .card--lead, so
   the card's own 17px padding already insets it like every other child. */
.card--lead .card-link { margin-top: 0.25rem; color: var(--teal); }
/* Wide cards get their own compact run-waterfall / RAG-panel sizing — a
   notch roomier than the .svc-mini treatment below, since they have a full
   half-width column to work with instead of a third. */
.services-lead-row .card-run,
.services-lead-row .rag-panel { padding: 0.625rem 0.75rem; font-size: var(--text-min); }
.services-lead-row .run-head,
.services-lead-row .rag-panel-head { padding-bottom: 0.375rem; }
.services-lead-row .run-rows { gap: 0.3125rem; padding: 0.375rem 0; }
.services-lead-row .run-label { font-size: var(--text-min); }
.services-lead-row .run-track { height: 0.375rem; }
.services-lead-row .run-axis { --tick-gap: 0.3125rem; }
.services-lead-row .rag-panel-query { margin: 0.4375rem 0 0.375rem; padding: 0.375rem 0.5625rem; }
.services-lead-row .rag-panel-sources { gap: 0.25rem; margin-bottom: 0.4375rem; }
.services-lead-row .rag-panel-answer { padding-top: 0.4375rem; }

/* The six non-AI cards run taller and narrower than the wide pair above —
   deliberately, so a 3-up row of them carries roughly the same visual
   weight as the 2-up row of wider cards, without matching them card for
   card. Grid row siblings stretch to the tallest card in their row (Quality
   Engineering's 4-row waterfall, most often), and content this varied in
   natural height — an 18-seat roster is shorter than a 4-row waterfall —
   would otherwise leave a lopsided gap under the shorter ones if pinned to
   the top; centering the whole icon-through-link block instead spreads
   that leftover space evenly above and below, so every card still looks
   intentionally composed at whatever height its row settles on. */
/* .svc-diagram (base) stays — reused by services/agentic-ai/ for its own
   diagrams. Everything family-specific that used to sit here (.card--inst,
   theme-eng/qa/teams, .diagram-stack/plan/model/dots/band/ramp, the "New"
   badge) was homepage-only and was retired along with the six practice
   cards that used it — see .svc / .scope below, the unified dark treatment
   that replaced them. */
.svc-diagram {
	margin: 1rem 0 1.125rem; padding: 0.9375rem 1rem; border-radius: 0.625rem;
	font-family: var(--font-mono); font-size: var(--text-min);
	background: rgba(16,22,25,.035); border: 1px solid var(--border);
}

/* ==========================================================================
   Unified dark instrument rack — the six practice cards' replacement for
   the four-way tinted-card theming above. One chassis (the same dark token
   block as .card--lead, so the two AI cards need no changes to sit beside
   these) with a per-card --signal colour driving the readout instead of the
   card background. Piloted at docs/design/services-unified-dark.html before
   landing here — see that file's history for the fixes (dash-draw animation
   removed after it silently truncated several connector lines; captions
   trimmed to one per card after they'd piled up to three or four saying the
   same thing). Scoped under #services, the only place these classes are
   used, even though none of the names collide with anything else in this
   file. */
#services .svc {
	--background: var(--ink);
	--foreground: #EAF3F1;
	--card: var(--ink-raise);
	--popover: rgba(255,255,255,.05);
	--muted-foreground: var(--lite-72);
	--accent: rgba(93,160,172,.16);
	--accent-foreground: var(--teal);
	--border: rgba(234,243,241,.14);
	/* --lite-45 is below AA on these two dark surfaces; this is the same
	   colour at an alpha that clears it, for the small mono labels. */
	--lite-60: rgba(234,243,241,.60);
	--signal: var(--teal);

	position: relative; overflow: hidden;
	display: flex; flex-direction: column;
	background: var(--background); color: var(--foreground);
	border: 1px solid var(--border); border-radius: var(--radius);
	padding: 1.375rem 1.375rem 1.25rem; min-height: 26.875rem;
	transition: transform .35s var(--ease), box-shadow .35s var(--ease), border-color .35s var(--ease);
}
#services .svc:hover { transform: translateY(-0.375rem); border-color: var(--signal); box-shadow: 0 30px 70px -40px rgba(0,0,0,.9); }

/* A single wash keyed to the card's own signal colour, sitting under the
   content. Same hue as the instrument, so the card and its readout agree —
   this is what replaces the six-way background tinting, at a strength
   where it reads as depth rather than as a category code. */
#services .svc::before {
	content: ''; position: absolute; inset: 0; pointer-events: none;
	background: radial-gradient(120% 80% at 88% -10%, var(--signal), transparent 62%);
	opacity: .1; transition: opacity .35s var(--ease);
}
#services .svc:hover::before { opacity: .17; }
#services .svc > * { position: relative; z-index: 1; }

#services .svc-icon {
	width: 2.625rem; height: 2.625rem; border-radius: 0.75rem;
	display: flex; align-items: center; justify-content: center;
	background: var(--accent); color: var(--accent-foreground);
	margin-bottom: 1rem;
}
#services .svc-icon svg { width: 1.1875rem; height: 1.1875rem; }
#services .svc h3 {
	font-size: 1.02rem; margin: 0 0 0.5rem;
	font-family: var(--font-display); font-variation-settings: 'wdth' 110, 'wght' 680;
}
#services .svc-desc { margin: 0; font-size: .89rem; line-height: 1.5; color: var(--muted-foreground); }

/* Teal regardless of the card's own --signal — the CTA is one consistent
   "go here next" colour across all eight cards, not a per-family accent;
   --signal stays scoped to the diagram/icon, which is where family
   identity actually lives now. */
#services .svc-link {
	display: inline-flex; align-items: center; gap: 0.375rem; margin-top: 1rem;
	font-size: .84rem; font-weight: 600; color: var(--teal);
}
#services .svc-link svg { transition: transform .3s var(--ease); }
#services .svc:hover .svc-link svg { transform: translateX(0.25rem); }

/* The whole card opens its practice, as on the case-study index -- but
   through the heading, not by wrapping the card in <a>. Every card holds
   a live panel or diagram with its own labels, so a card-sized <a> would
   announce all of it as one link name. Here the link is the practice's
   name, and its ::after stretches over the card so a click anywhere lands
   on it. "Learn more" (.card-link / .svc-link) stays as the visual cue
   only, a span hidden from screen readers, the way "Read case study"
   sits inside .cs-card. One link and one tab stop per card; the focus ring
   is drawn on the card, since that is what the link now covers. */
#services :is(.card--lead, .svc) h3 > a::after { content: ''; position: absolute; inset: 0; z-index: 2; }
#services :is(.card--lead, .svc) h3 > a:focus-visible { outline: none; }
#services :is(.card--lead, .svc):has(h3 > a:focus-visible) { outline: 2px solid var(--ring); outline-offset: 3px; }
/* .svc lifts every child onto z-index 1, above its colour wash. The heading
   is exempt: it must be unpositioned so its link's ::after measures against
   the card, and its z-index must be auto, because a flex item with any
   other z-index is a stacking context even when static, which would trap
   the ::after beneath the diagram and cue that follow it. The wash moves
   under the content instead: isolation makes the card its own stacking
   context, and z-index -1 keeps the wash above the card's background but
   below everything in it. */
#services .svc { isolation: isolate; }
#services .svc::before { z-index: -1; }
#services .svc > h3 { position: static; z-index: auto; }

/* Every readout sits in the same recessed panel: same inset surface, same
   mono header, same caption slot. The panel is the constant; the diagram
   inside is the variable. */
#services .scope {
	margin-top: 1rem; margin-bottom: auto;
	background: var(--popover); border: 1px solid var(--border);
	border-radius: 0.75rem; padding: 0.6875rem 0.8125rem 0.625rem;
}
#services .scope-head {
	display: flex; justify-content: space-between; align-items: center; gap: 0.625rem;
	font-family: var(--font-mono); font-size: var(--text-min); letter-spacing: .04em;
	color: var(--lite-60); padding-bottom: 0.5rem; border-bottom: 1px solid var(--border);
}
#services .scope-head b { color: var(--signal); font-weight: 500; }
#services .scope svg.plot { display: block; width: 100%; height: auto; margin: 0.5625rem 0 0; overflow: visible; }
#services .scope-cap {
	margin: 0.5625rem 0 0; font-family: var(--font-mono); font-size: var(--text-min); line-height: 1.45;
	/* Not --lite-45: that lands at 4.11:1 against the recessed panel, just
	   under AA, and these captions carry the actual proof line. --lite-60
	   is the same colour at 5.99:1. */
	color: var(--lite-60);
}
#services .scope-ok {
	display: flex; align-items: center; gap: 0.375rem; margin-top: 0.5rem; padding-top: 0.5rem;
	border-top: 1px solid var(--border);
	font-family: var(--font-mono); font-size: var(--text-min); color: var(--signal);
}
/* Outline-vs-filled dots in a diagram read as arbitrary without this — a
   legend earns its space whenever a shape alone is asked to carry meaning. */
#services .scope-legend { display: flex; gap: 1rem; margin-top: 0.5rem; font-family: var(--font-mono); font-size: var(--text-min); color: var(--lite-60); }
#services .scope-legend span { display: inline-flex; align-items: center; gap: 0.375rem; }
#services .scope-legend i { width: 0.4375rem; height: 0.4375rem; border-radius: 50%; flex: none; }
#services .scope-legend i.dot-outline { border: 1.4px solid var(--border); }
#services .scope-legend i.dot-fill { background: var(--signal); }

/* A live instrument reads as live. One slow pulse per card on the element
   that is genuinely "current" in that readout — not a decorative shimmer on
   everything. */
#services .svc .pulse { animation: scope-pulse 2.6s var(--ease) infinite; transform-origin: center; }
@keyframes scope-pulse {
	0%, 100% { opacity: 1; }
	50% { opacity: .35; }
}
@media (prefers-reduced-motion: reduce) {
	#services .svc .pulse { animation: none; }
	#services .svc:hover { transform: none; }
}

/* Shared plot primitives */
#services .plot text { font-family: var(--font-mono); fill: var(--lite-60); }
#services .plot .lbl { font-size: 8.5px; }
#services .plot .lbl-on { font-size: 8.5px; fill: #EAF3F1; }
#services .plot .wire { stroke: var(--border); stroke-width: 1; fill: none; }
#services .plot .wire-on { stroke: var(--signal); stroke-width: 1.4; fill: none; }
#services .plot .node { fill: none; stroke: var(--border); stroke-width: 1.2; }
#services .plot .node-on { fill: none; stroke: var(--signal); stroke-width: 1.6; }
#services .plot .chip { fill: rgba(255,255,255,.04); stroke: var(--border); stroke-width: 1; }
#services .plot .chip-on { fill: color-mix(in srgb, var(--signal) 16%, transparent); stroke: var(--signal); stroke-width: 1.2; }

@media (max-width: 61.25em) { /* 980px */
	#services .svc { min-height: 21.25rem; }
}
@media (max-width: 45em) { /* 720px */
	#services .svc { min-height: 0; }
}

/* Run waterfall — the lead card's argument, drawn rather than typed. Four
   spans of one real agent run share a single time axis, so the card's claim
   ("human checkpoints where you need them") is carried by proportion: the
   three machine spans are sub-2s ticks pinned to the left edge, and the
   approval hold sweeps 96.8% of the run. Each row's --start/--len are the
   true percentages of the 207s run, set inline in the markup.

   Colour note: --beam (#2B34E0) sits at ~2.3:1 on this card's ink surface,
   so it carries none of the marks here — teal, butter and the green read at
   6:1 or better. Cobalt stays on the lead tag and the link, where it is
   large enough to hold. */
.card-run {
	align-self: stretch; display: flex; flex-direction: column;
	border: 1px solid var(--border); border-radius: 0.75rem; background: rgba(4,10,14,.5);
	padding: 1.25rem; font-family: var(--font-mono); color: var(--lite-72);
}
.run-head {
	display: flex; justify-content: space-between; align-items: baseline; gap: 0.75rem; text-wrap: balance;
	padding-bottom: 0.8125rem; border-bottom: 1px solid var(--border);
	font-size: var(--text-min); letter-spacing: .13em; text-transform: uppercase;
	color: rgba(234,243,241,.42);
}
/* The eyebrow is uppercased, but the duration is not — "3M 27S" reads as an
   ambiguous unit where "3m 27s" reads as a time. */
/* flex: none keeps a duration whole ("3m 48s", never "3m" over "48s") when a
   narrow card makes the label wrap beside it. */
.run-total { flex: none; text-transform: none; color: #EAF3F1; letter-spacing: .06em; font-variant-numeric: tabular-nums; }

.run-rows { flex: 1; display: flex; flex-direction: column; justify-content: center; gap: 1.0625rem; padding: 1.25rem 0; }
.run-row { display: grid; grid-template-columns: 1fr auto; gap: 0.1875rem 0.625rem; }
.run-label { font-size: 0.75rem; color: var(--lite-72); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.run-dur { font-size: var(--text-min); color: rgba(234,243,241,.36); font-variant-numeric: tabular-nums; }
.run-track {
	grid-column: 1 / -1; position: relative; overflow: hidden;
	height: 0.5rem; border-radius: 0.25rem; background: rgba(234,243,241,.06);
}
.run-span {
	position: absolute; top: 0; bottom: 0; left: var(--start);
	width: var(--len); min-width: 0.375rem; border-radius: 0.25rem; background: var(--teal);
}
/* The hold is the one thing this panel is about, so it gets the only warm
   mark and the only emphasised label. */
.run-row--hold .run-label { color: var(--butter); }
.run-row--hold .run-span { background: var(--butter); }
.run-row--ok .run-span { background: #7BE0A8; }

/* Ticks sit at their true offsets (1m = 29% of 207s), not spread evenly —
   an evenly spread axis would quietly misreport the proportion the whole
   panel exists to show. Labels are left-aligned from the tick so neither
   end clips. */
/* The axis box is exactly one line of tick labels tall, under a --tick-gap,
   so the labels (positioned absolutely, at --at along the track) stay inside
   it at any text size and the footer line below can't run into them. It
   used to be a fixed 15px box with the labels hanging out of it, which the
   homepage's compact cards squeezed to 10px -- labels over the footer. */
.run-axis {
	--tick-gap: 0.6875rem;
	position: relative; box-sizing: content-box; height: 1.2rem; height: 1lh; padding-top: var(--tick-gap);
	border-top: 1px solid var(--border); font-size: var(--text-min); color: rgba(234,243,241,.34);
}
.run-axis span { position: absolute; top: var(--tick-gap); left: var(--at); }
.run-axis span::before {
	content: ''; position: absolute; top: calc(-1 * var(--tick-gap)); left: 0; width: 1px; height: 0.3125rem;
	background: rgba(234,243,241,.18);
}
.run-foot { margin-top: 0.125rem; font-size: var(--text-min); color: rgba(234,243,241,.36); }

/* Entry: rows stagger in, then only the hold visibly sweeps — the machine
   spans are already at their 6px floor, so nothing about them animates.
   That asymmetry is the point. */
.card-run .run-row { opacity: 0; transform: translateY(0.375rem); }
.card-run .run-span { width: 0; }
.card-run.is-running .run-row {
	opacity: 1; transform: none;
	transition: opacity .45s var(--ease), transform .45s var(--ease);
	transition-delay: calc(var(--i) * .08s);
}
.card-run.is-running .run-span {
	width: var(--len);
	transition: width .6s var(--ease);
	transition-delay: calc(var(--i) * .08s + .1s);
}
.card-run.is-running .run-row--hold .run-span { transition-duration: 1.1s; }
.card-run.is-running .run-row--ok .run-span { transition-delay: 1.35s; }
@media (prefers-reduced-motion: reduce) {
	.card-run .run-row { opacity: 1; transform: none; }
	.card-run .run-span { width: var(--len); }
	/* Repeated with .is-running so these outrank the two-class transition
	   rules above — a plain .card-run .run-span here would lose to them on
	   specificity and the animation would still play. */
	.card-run .run-row, .card-run.is-running .run-row,
	.card-run .run-span, .card-run.is-running .run-span { transition: none; }
}

/* Generative AI's co-lead visual — same dark inset-panel language as
   .card-run (border, rgba(4,10,14,.5) fill, mono type) so the two lead
   cards read as one family, but static: a RAG answer is a lookup and a
   citation, not a multi-step process with a duration worth animating. */
.rag-panel {
	align-self: stretch; border: 1px solid var(--border); border-radius: 0.75rem;
	background: rgba(4,10,14,.5); padding: 1.25rem; font-family: var(--font-mono);
	color: var(--lite-72); font-size: 0.78125rem;
}
.rag-panel-head {
	display: flex; align-items: center; gap: 0.375rem; padding-bottom: 0.8125rem;
	border-bottom: 1px solid var(--border); font-size: var(--text-min); letter-spacing: .06em;
	color: rgba(234,243,241,.42);
}
.rag-panel-head svg { flex: none; }
.rag-panel-query {
	margin: 1rem 0 0.875rem; padding: 0.625rem 0.75rem; background: rgba(234,243,241,.05);
	border-radius: 0.5rem; color: #EAF3F1; font-size: 0.78125rem; line-height: 1.5;
}
.rag-panel-sources { list-style: none; margin: 0 0 0.875rem; padding: 0; display: flex; flex-direction: column; gap: 0.4375rem; }
.rag-panel-sources li { display: flex; align-items: center; gap: 0.5rem; font-size: var(--text-min); color: var(--lite-72); }
.rag-panel-sources li::before { content: ''; width: 0.25rem; height: 0.25rem; border-radius: 50%; background: var(--teal); flex: none; }
.rag-panel-answer {
	display: flex; align-items: center; gap: 0.4375rem; padding-top: 0.8125rem;
	border-top: 1px solid var(--border); font-size: var(--text-min); color: #7BE0A8;
}
.rag-panel-answer svg { flex: none; }

/* Same icon/heading/body/link type scale on every one of the eight cards,
   .card--lead included — deliberately, so the wide top pair reads as a
   different shape, never a different rank. Sized snugger than the shared
   .card default (without touching .card itself, which the Why Us / Awards
   grids still rely on as-is). */
.services-grid .card-icon {
	width: 2.375rem; height: 2.375rem; border-radius: 0.625rem; margin-bottom: 1rem;
}
.services-grid .card-icon svg { width: 1.125rem; height: 1.125rem; }
.services-grid .card h3 { font-size: 1.02rem; margin-bottom: 0.5rem; }
.services-grid .card p { font-size: .89rem; line-height: 1.5; }
.services-grid .card .card-link { margin-top: 0.875rem; font-size: .84rem; }

/* ==========================================================================
   Services hub (/services/) — practice map + practice grid.

   The homepage's services-grid gets one lead card because its job is to
   hook with a single practice. This page's job is the opposite: it's the
   directory, so all seven practices need equal legibility. Instead of an
   asymmetric lead card, every practice gets the same richer treatment —
   a crafted cover (the case-study gradient technique, not stock photography,
   which would fight the site's own instrument-panel visual language), one
   real, sourced proof point, and its sub-services as scannable chips rather
   than a bullet list. The one bold move lives in the practice map below,
   not repeated seven times in the grid.
   ========================================================================== */

/* --- practice map: the page's signature element — every practice as one
   connected system, and a working jump-nav at the same time. ------------- */
.practice-map { padding: 0.25rem 0 0.5rem; }
.practice-map-track {
	position: relative; display: flex; align-items: flex-start;
	gap: 0.25rem; overflow-x: auto; padding: 1.375rem 0.25rem 0.375rem; margin: 0 -0.25rem;
	-webkit-overflow-scrolling: touch;
}
.practice-map-track::before {
	content: ''; position: absolute; left: 5%; right: 5%; top: 2.0625rem; height: 1px;
	background: var(--border); z-index: 0;
}
.practice-map-track a {
	position: relative; z-index: 1; flex: 1 0 auto; min-width: 6.75rem;
	display: flex; flex-direction: column; align-items: center; gap: 0.6875rem;
	padding: 0 0.375rem; text-align: center;
	font-family: var(--font-mono); font-size: var(--text-min); letter-spacing: .02em;
	color: var(--muted-foreground); white-space: nowrap; transition: color .2s ease;
}
.practice-map-dot {
	width: 0.75rem; height: 0.75rem; border-radius: 50%;
	background: var(--surface); border: 2px solid var(--dot, var(--teal));
	transition: transform .3s var(--spring), box-shadow .3s ease;
}
.practice-map-track a:hover, .practice-map-track a:focus-visible { color: var(--foreground); }
.practice-map-track a:hover .practice-map-dot, .practice-map-track a:focus-visible .practice-map-dot {
	transform: scale(1.4); box-shadow: 0 0 0 6px var(--dot-glow, rgba(93,160,172,.2));
}

/* --- the grid: seven equal practices, richer than a plain text card ----- */
/* Four tracks, each card spanning two — a two-up grid that can also place
   a card on the half-column. See the centering rule below; without that
   this would just be repeat(2, 1fr). */
.practice-grid { grid-template-columns: repeat(4, 1fr); gap: 1.5rem; margin-top: 0.5rem; }
.practice-grid > .card--practice { grid-column: span 2; }
.card--practice { padding: 1.75rem; scroll-margin-top: 7rem; }
.card--practice h3 { font-size: 1.18rem; margin-bottom: 0.625rem; }
.card--practice > p { font-size: .95rem; }

/* Odd card count means the last card (currently Quantitative Analytics
   Testing) would sit alone in the final row, flush left, with a
   conspicuous empty column beside it. :nth-child(odd):last-child only
   matches when the last item is actually unpaired, so this stays correct
   on its own if a practice is ever added or removed.

   Spanning tracks 2-3 puts it dead centre while keeping the exact width
   its siblings have — both work out to (container + 24px) / 4 at any
   width, so it stays centred responsively with no magic numbers.

   The previous version spanned the full row and capped the width back with
   max-width plus auto margins. That also worked; this is preferred because
   the card's grid area is now its real rendered width, which keeps the
   subgrid track sizing below straightforward to reason about instead of
   depending on how a percentage max-width resolves against a wider area. */
.practice-grid > .card--practice:nth-child(odd):last-child { grid-column: 2 / span 2; }

.practice-cover {
	position: relative; aspect-ratio: 3 / 1; margin: -1.75rem -1.75rem 1.375rem; overflow: hidden;
}
.practice-cover .cover-bg { display: block; width: 100%; height: 100%; }
.practice-cover-icon {
	position: absolute; inset: 0; display: flex; align-items: center; padding-left: 1.625rem;
}
.practice-cover-icon svg {
	width: 2rem; height: 2rem; color: #fff; opacity: .95;
	filter: drop-shadow(0 2px 10px rgba(0,0,0,.28));
}

.practice-proof {
	display: flex; flex-direction: column; gap: 0.1875rem; margin-top: 1.125rem;
	padding: 0.8125rem 1rem; background: var(--accent); border: 1px solid var(--border);
	border-radius: 0.75rem; transition: border-color .2s ease, background .2s ease;
}
/* Only a linked box reacts to the pointer: a box that states a fact
   without a page behind it stays still. */
a.practice-proof:hover { border-color: var(--teal); background: var(--muted); }
.practice-proof-tag {
	font-family: var(--font-mono); font-size: var(--text-min); font-weight: 500;
	letter-spacing: .12em; text-transform: uppercase; color: var(--teal-deep); opacity: .85;
}
.practice-proof-figure {
	font-family: var(--font-display); font-variation-settings: 'wdth' 106, 'wght' 740;
	font-size: 1.2rem; letter-spacing: -.01em; color: var(--foreground);
}
.practice-proof-label { font-size: .82rem; line-height: 1.4; color: var(--muted-foreground); }
.practice-proof-label b { font-weight: 600; color: var(--teal-deep); }

/* Sub-service chips replace a bullet list — the point of the page is that
   every one of these is a real, clickable link, so they read as a set of
   doors to open, not a summary to skim past. The Industries hub's chips
   are the exception: .card-sublinks--static puts the same chip on a plain
   <li>, and with no hover rule it doesn't pretend to be a link. */
.card-sublinks { list-style: none; margin: 1.125rem 0 0; padding: 0; display: flex; flex-wrap: wrap; gap: 0.5rem; }
.card-sublinks a, .card-sublinks--static li {
	display: inline-flex; align-items: center; padding: 0.4375rem 0.875rem; border-radius: 999px;
	background: var(--popover); border: 1px solid var(--border);
	font-size: var(--text-min-sans); font-weight: 600; color: var(--ink-70);
	transition: border-color .2s ease, color .2s ease, background .2s ease;
}
.card-sublinks a:hover { border-color: var(--teal); color: var(--teal-deep); background: var(--accent); }

/* --- bottom-aligned card tail -------------------------------------------
   .card--practice was display:block, so every band under the fixed 3:1
   cover flowed from the top and stopped wherever the content above it ran
   out. Measured across one row: the Explore links sat 30px and 99px above
   their card bottoms (AI Native vs Cloud Engineering), a 69px spread, and
   the filled .practice-proof boxes sat 25px out of register side by side.

   Same two-tier fix .discipline-grid uses on /company/about-us/.

   Tier 1, flex — pushes the link alone to the bottom. The sub-link block
   keeps an 18px bottom margin so that gap can't collapse to zero on the
   tallest card in a row, where there's no slack left for the auto margin
   to absorb.

   The sub-link block is deliberately not bottom-anchored along with it in
   this tier. That block opens with a border-top hairline and the blocks
   are different heights, so hanging them from a shared bottom edge leaves
   that visible rule floating at a different height in each card — worse
   than where it already sits. A rule is a structural marker; it shouldn't
   wander. So the slack pools in one gap above the Explore link instead,
   where air reads as breathing room before a CTA. */
.card--practice { display: flex; flex-direction: column; }
.card--practice .card-sublinks { margin-bottom: 1.125rem; }
.card--practice .card-link { margin-top: auto; align-self: start; }

/* Tier 2, subgrid — the same mechanism .discipline-grid uses on
   /company/about-us/. Every band gets a shared row track, so descriptions,
   proof boxes, sub-link hairlines and links all line up across a row and
   the slack is distributed into whichever band actually varies rather than
   pooling in one gap. Flex alone leaves the filled .practice-proof boxes
   24px out of register and the hairlines up to 43px out; those are the
   misalignments you read before you notice the link.

   row-gap has to drop to 0 here: under subgrid it would otherwise apply
   between all six bands inside every card, not just between card rows, and
   the internal rhythm already comes from each band's own margins. Card
   rows get their 24px back as a bottom margin — except the last card,
   which has no row beneath it. */
@supports (grid-template-rows: subgrid) {
	.practice-grid { grid-template-rows: repeat(6, auto); row-gap: 0; }
	.practice-grid > .card--practice {
		display: grid; grid-template-rows: subgrid; grid-row: span 6; margin-bottom: 1.5rem;
	}
	.practice-grid > .card--practice:last-child { margin-bottom: 0; }
	.practice-grid .card--practice .card-link { margin-top: 0; justify-self: start; }
}

/* --- Staff Augmentation: structurally different (people, not sub-services),
   so it closes the grid as its own dark band rather than sitting as the odd
   card left over from a 7-into-2-columns grid. */
.practice-close {
	margin-top: 1.5rem; display: grid; grid-template-columns: 1.15fr .85fr; gap: 2.5rem; align-items: center;
	padding: 2.5rem 2.75rem;
	--background: var(--ink); --foreground: #EAF3F1; --surface: var(--ink);
	--card: var(--ink-raise); --muted-foreground: var(--lite-72);
	--accent: rgba(43,52,224,.16); --border: rgba(234,243,241,.14);
	background: var(--background); color: var(--foreground);
	border-radius: var(--radius-lg); scroll-margin-top: 7rem;
}
.practice-close-tag {
	display: block; font-family: var(--font-mono); font-size: var(--text-min); font-weight: 500;
	letter-spacing: .16em; text-transform: uppercase; color: var(--butter); margin-bottom: 1rem;
}
.practice-close h3 { font-size: clamp(1.35rem, 2vw, 1.65rem); margin-bottom: 0.75rem; }
.practice-close > div > p { font-size: 1rem; max-width: 44ch; color: var(--lite-72); }
.practice-close .card-link { color: var(--teal); margin-top: 1.25rem; }
.practice-close-fact {
	margin-top: 1.125rem; font-family: var(--font-mono); font-size: .82rem; color: rgba(234,243,241,.5);
}

.roster { display: grid; grid-template-columns: repeat(6, 1fr); gap: 0.625rem; }
.roster span {
	aspect-ratio: 1; border-radius: 50%; background: var(--ink-raise);
	border: 1.5px solid var(--seat, var(--border));
}
.roster span.is-on { background: var(--seat, var(--teal)); border-color: transparent; }

@media (max-width: 61.25em) { /* 980px */
	.practice-close { grid-template-columns: 1fr; padding: 2.125rem; }
	.roster { grid-template-columns: repeat(8, 1fr); max-width: 20rem; }
}
@media (max-width: 53.75em) { /* 860px */
	.practice-grid { grid-template-columns: 1fr; }
	/* One column, so there's no orphan to centre and nothing to span. Both
	   spans have to be cleared, not just the centring one: a leftover
	   "span 2" against a single declared track makes the grid generate an
	   implicit second column and the row collapses to half width. */
	.practice-grid > .card--practice,
	.practice-grid > .card--practice:nth-child(odd):last-child { grid-column: auto; }
}
@media (max-width: 35em) { /* 560px */
	.practice-map-track::before { display: none; }
	.practice-map-track { justify-content: flex-start; }
	.practice-map-track a { min-width: 5.75rem; }
}

/* ==========================================================================
   Stats + feature split (Why us)
   ========================================================================== */
.stat-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1.5rem; margin-bottom: 3.5rem; text-align: center; }
.stat-row strong { display: block; font-family: var(--font-display); font-variation-settings: 'wdth' 108, 'wght' 720; font-size: clamp(1.8rem, 4vw, 2.3rem); letter-spacing: -.03em; font-variant-numeric: tabular-nums; }
.section--dark .stat-row strong { color: #fff; }
.stat-row span { color: var(--muted-foreground); font-size: .88rem; }
/* Standalone stats band (About). Two things go wrong when .stat-row is a
   section's only content instead of the header for a feature grid: the 56px
   margin below it — there to separate it from that grid — becomes dead space
   under an 85px-tall row inside 96px of section padding, and four 1fr lanes
   across the full 1217px container leave ~130px of air flanking each stat, so
   they read as four unrelated numbers rather than one band. Cap the width so
   they group, and drop the trailing margin. Inert below ~980px, where the
   container is already narrower than the cap and the row is two-up. */
.stat-row--band { max-width: 53.75rem; margin-inline: auto; margin-bottom: 0; }
/* About page only: the band directly under the hero reads tighter than a
   full .section gap on both sides (hero photo above, story section below),
   so it gets its own shorter padding rather than the sitewide 96px. */
.section--stat-band { padding: 4.375rem 0; }

/* ==========================================================================
   Technology stack — two alternating washes across the seven rows (teal
   ink, then the neutral Cloud & DevOps ink), starting with teal. The label
   sits in a text-safe "-ink" variant of each hue rather than the flat
   --ink-45 grey it used before, which measured ~3:1 on this section's mist
   background — under the 4.5:1 small-text floor and the reason it read as
   "almost invisible." */
.tech-row {
	display: grid; grid-template-columns: 10rem 1fr; gap: 1.5rem; align-items: center;
	padding: 1.25rem 1.25rem; border-radius: var(--radius);
	background: var(--tech-tint); border: 1px solid var(--tech-tint-border);
}
.tech-row + .tech-row { margin-top: 0.75rem; }
.tech-row .label { font-family: var(--font-mono); font-size: var(--text-min); font-weight: 600; letter-spacing: .1em; text-transform: uppercase; color: var(--tech-label); }
.tech-row .tags { display: flex; flex-wrap: wrap; gap: 0.5rem; }

.tech-row--ai       { --tech-tint: rgba(93,160,172,.16);  --tech-tint-border: rgba(93,160,172,.28);  --tech-label: var(--teal-ink); }
.tech-row--frontend { --tech-tint: rgba(16,22,25,.055);   --tech-tint-border: rgba(16,22,25,.16);    --tech-label: var(--ink); }
.tech-row--backend  { --tech-tint: rgba(93,160,172,.16);  --tech-tint-border: rgba(93,160,172,.28);  --tech-label: var(--teal-ink); }
.tech-row--devops   { --tech-tint: rgba(16,22,25,.055);   --tech-tint-border: rgba(16,22,25,.16);    --tech-label: var(--ink); }
.tech-row--database { --tech-tint: rgba(93,160,172,.16);  --tech-tint-border: rgba(93,160,172,.28);  --tech-label: var(--teal-ink); }
.tech-row--qa        { --tech-tint: rgba(16,22,25,.055);   --tech-tint-border: rgba(16,22,25,.16);    --tech-label: var(--ink); }
.tech-row--bi        { --tech-tint: rgba(93,160,172,.16);  --tech-tint-border: rgba(93,160,172,.28);  --tech-label: var(--teal-ink); }
.tech-row .tags span { padding: 0.4375rem 0.8125rem; border-radius: 999px; background: var(--popover); border: 1px solid var(--border); font-size: .85rem; }
/* Fixed 160px label column leaves the tags column only ~140px to wrap in
   at phone width, forcing a single-file stack of pills instead of a
   natural multi-per-line flow. Label moves to its own row above the tags
   so they get the full row width to wrap into. */
@media (max-width: 45em) { /* 720px */
	.tech-row { grid-template-columns: 1fr; gap: 0.625rem; }
}

/* ==========================================================================
   Case studies
   ========================================================================== */
/* Tighter below the text than around it (bottom padding 1.375rem, the
   cover a 15% shorter band than 16:9): in the carousel all seven cards
   take the tallest one's height, and at the old spacing that read long. */
.case-card { display: flex; flex-direction: column; background: var(--card); border: 1px solid var(--border); border-radius: var(--radius); padding: 1.875rem 1.875rem 1.375rem; transition: transform .35s var(--ease), box-shadow .35s; position: relative; overflow: hidden; }
.case-card:hover { transform: translateY(-0.375rem) !important; box-shadow: var(--shadow-soft); }
.case-card-media { position: relative; aspect-ratio: 16 / 7.65; margin: -1.875rem -1.875rem 1.5rem; overflow: hidden; }
.case-card-media .cover-bg { display: block; width: 100%; height: 100%; }
.case-card-icon { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; }
.case-card-icon svg { width: 2.75rem; height: 2.75rem; color: #fff; opacity: .92; filter: drop-shadow(0 2px 8px rgba(0,0,0,.25)); }
.case-tag { display: inline-block; font-family: var(--font-mono); font-size: var(--text-min); font-weight: 500; letter-spacing: .1em; text-transform: uppercase; color: var(--teal-deep); margin-bottom: 0.875rem; }
.case-card h3 { font-size: 1.25rem; margin-bottom: 0.75rem; }
.case-card p.excerpt { color: var(--muted-foreground); margin-bottom: 1rem; }
/* Pushed to the bottom of the card as a fixed-spacing unit with the CTA
   below it — title/excerpt length varies card to card, so letting stats+CTA
   float in normal flow put "Read case study" at a different height on every
   card. Anchoring here means only the copy above ever stretches. */
.case-stats { display: grid; grid-template-columns: repeat(2, 1fr); gap: 0.75rem; margin-top: auto; margin-bottom: 0.75rem; }
.case-card .card-link { margin-top: 0.5rem; }
.case-stats strong { display: block; font-family: var(--font-display); font-variation-settings: 'wdth' 108, 'wght' 700; font-size: 1.35rem; color: var(--sun); }
.case-stats span { font-size: var(--text-min-sans); color: var(--muted-foreground); }

/* Homepage coverflow carousels -- every case study ("Featured work") and
   every blog post ("From the blog"), modelled on goavega.com's "Triumph
   Tales" (Swiper's coverflow: rotate 50deg, three across, one card on
   phones). The current card sits flat in the middle, its neighbours turn
   50deg away with the far edge shaded, and the rest wait out of sight
   either side. The cards themselves (.case-card, .blog-teaser-card) are
   untouched: everything here is on the .coverflow-slide around them, so a
   card's own hover lift (a !important transform) never fights the slide's
   position. initCoverflows() in main.js sets one data-pos per slide (-2..2,
   its distance from the current one around the loop) and every state
   below keys off it.

   Without the script it's a plain row of cards that scrolls sideways. */
.coverflow-track { display: flex; gap: 1.5rem; overflow-x: auto; scroll-snap-type: x mandatory; }
.coverflow-slide { position: relative; display: flex; flex: 0 0 min(25rem, 85%); scroll-snap-align: center; }
.coverflow-slide > * { flex: 1 1 auto; min-width: 0; }

/* Every slide stacks in one grid cell, so the row is as tall as the
   tallest card and every card stretches to it; transforms then spread them out.
   overflow-x: clip keeps the slides waiting off to the side from widening
   the page, while the turned cards may still poke above and below. */
/* Three across, a side card's outer edge lands ~1.56 card-widths from the
   centre, wider than the content column: like the reference, the stage
   runs the full width of the section (still centred on the column, so the
   middle card doesn't move) and a card is 28% of it, 400px at most. The
   turned cards' outer edges also come ~7% of a card's height toward the
   viewer, so they reach that far above and below the middle card; the top
   margin here and the dots' larger one give them that room. */
.coverflow.is-ready .coverflow-stage {
	position: relative; overflow-x: clip; touch-action: pan-y;
	margin: 1.25rem calc(50% - 50vw) 0;
}
.coverflow.is-ready .coverflow-track {
	display: grid; justify-items: center; overflow: visible; scroll-snap-type: none; perspective: 75rem;
}
.coverflow.is-ready .coverflow-slide {
	grid-area: 1 / 1; width: min(25rem, 28%);
	transition: transform .5s var(--ease), opacity .5s var(--ease);
}
.coverflow.is-ready .coverflow-slide[data-pos="0"] { z-index: 3; }
.coverflow.is-ready .coverflow-slide[data-pos="-1"] { z-index: 2; transform: translateX(-108%) rotateY(50deg); }
.coverflow.is-ready .coverflow-slide[data-pos="1"] { z-index: 2; transform: translateX(108%) rotateY(-50deg); }
.coverflow.is-ready .coverflow-slide[data-pos="-2"] { transform: translateX(-216%) rotateY(50deg); opacity: 0; pointer-events: none; }
.coverflow.is-ready .coverflow-slide[data-pos="2"] { transform: translateX(216%) rotateY(-50deg); opacity: 0; pointer-events: none; }
/* A side card is a target for "bring me to the middle", not a link. */
.coverflow.is-ready .coverflow-slide:not([data-pos="0"]) { cursor: pointer; }
.coverflow.is-ready .coverflow-slide:not([data-pos="0"]) > * { pointer-events: none; }
/* Depth: the edge turned away from the viewer darkens. */
.coverflow.is-ready .coverflow-slide::after {
	content: ''; position: absolute; inset: 0; z-index: 1; pointer-events: none;
	border-radius: var(--radius); opacity: 0; transition: opacity .5s var(--ease);
	background: linear-gradient(to right, rgba(6,14,17,0) 10%, rgba(6,14,17,.62));
}
.coverflow.is-ready .coverflow-slide[data-pos="1"]::after { background: linear-gradient(to left, rgba(6,14,17,0) 10%, rgba(6,14,17,.62)); }
.coverflow.is-ready .coverflow-slide:is([data-pos="-1"], [data-pos="1"])::after { opacity: 1; }
/* A slide wrapping round the back of the loop jumps instead of sweeping
   across behind the others. */
.coverflow .coverflow-slide.no-anim,
.coverflow .coverflow-slide.no-anim::after { transition: none; }

/* Arrows: hidden until the pointer is over the cards (or keyboard focus
   reaches them), as on the reference. Touch screens have no hover to
   reveal them, so there it's swipe and the dots instead. */
.coverflow-arrow {
	position: absolute; top: 50%; z-index: 4; transform: translateY(-50%);
	width: 3rem; height: 3rem; border-radius: 50%; display: flex; align-items: center; justify-content: center;
	background: var(--card); border: 1px solid var(--border); color: var(--foreground);
	opacity: 0; transition: opacity .3s ease, border-color .2s ease, color .2s ease;
}
.coverflow-arrow[hidden] { display: none; }
.coverflow-arrow--prev { left: 1.25rem; }
.coverflow-arrow--next { right: 1.25rem; }
.coverflow-arrow svg { width: 1.25rem; height: 1.25rem; }
.coverflow-arrow:hover { border-color: var(--teal); color: var(--teal); }
.coverflow-stage:hover .coverflow-arrow,
.coverflow-arrow:focus-visible { opacity: 1; }
@media (hover: none) { .coverflow-arrow { display: none; } }

/* Pause/Play and the dots. Each dot is drawn small but the button around
   it is a 24px target. */
.coverflow-nav { display: flex; align-items: center; justify-content: center; gap: 0.75rem; margin-top: 4rem; }
.coverflow-nav[hidden] { display: none; }
.coverflow-toggle {
	width: 2.25rem; height: 2.25rem; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center;
	border: 1px solid var(--border); color: var(--muted-foreground); transition: color .2s ease, border-color .2s ease;
}
.coverflow-toggle:hover { color: var(--foreground); border-color: var(--teal); }
.coverflow-icon-play { display: none; }
.coverflow:not(.is-playing) .coverflow-icon-play { display: block; }
.coverflow:not(.is-playing) .coverflow-icon-pause { display: none; }
.coverflow-dots { display: flex; align-items: center; }
.coverflow-dot { width: 1.5rem; height: 1.5rem; padding: 0; display: flex; align-items: center; justify-content: center; }
.coverflow-dot::before {
	content: ''; width: 0.5rem; height: 0.5rem; border-radius: 50%;
	background: var(--lite-45); transition: background .2s ease, transform .2s ease;
}
.coverflow-dot:hover::before { background: var(--lite-72); }
.coverflow-dot[aria-current="true"]::before { background: var(--teal); transform: scale(1.35); }
.coverflow-toggle:focus-visible,
.coverflow-arrow:focus-visible,
.coverflow-dot:focus-visible { border-radius: 50%; }

/* Too many cards for a dot each (the blog's 23): a window of five rides
   along instead, Swiper's "dynamic bullets". The current card is the big
   dot in the middle and the dots shrink toward the edges (data-d is a
   dot's distance from it). On every step the strip slides one place and
   the sizes follow, then it settles back, so it reads as an endless row.
   The strip holds seven: one waiting just outside each edge to slide in. */
.coverflow-dots--window { width: 7.5rem; overflow: hidden; }
.coverflow-dots-strip { display: flex; flex: none; margin-left: -1.5rem; transition: transform .5s var(--ease); }
.coverflow-dots-strip.no-anim,
.coverflow-dots-strip.no-anim .coverflow-dot::before { transition: none; }
.coverflow-dots--window .coverflow-dot::before { transition: background .5s var(--ease), transform .5s var(--ease); }
.coverflow-dots--window .coverflow-dot:is([data-d="-1"], [data-d="1"])::before { transform: scale(.85); }
.coverflow-dots--window .coverflow-dot:is([data-d="-2"], [data-d="2"])::before { transform: scale(.55); }
.coverflow-dots--window .coverflow-dot:is([data-d="-3"], [data-d="3"])::before { transform: scale(.25); }
.coverflow-dots--window .coverflow-dot[data-d="0"]::before { background: var(--teal); transform: scale(1.35); }
.coverflow-dots--window .coverflow-dot:not([data-d="0"])::before { background: var(--lite-45); }
.coverflow-dots--window .coverflow-dot:not([data-d="0"]):hover::before { background: var(--lite-72); }

/* One card from 980px down (phones and tablets): the neighbours slide
   and turn out of view instead of sitting beside it. */
@media (max-width: 61.25em) {
	.coverflow.is-ready .coverflow-stage { margin: 0; }
	.coverflow-nav { margin-top: 2rem; }
	.coverflow.is-ready .coverflow-slide { width: min(25rem, 100%); }
	.coverflow.is-ready .coverflow-slide[data-pos="-1"] { transform: translateX(calc(-100% - 3rem)) rotateY(50deg); opacity: 0; pointer-events: none; }
	.coverflow.is-ready .coverflow-slide[data-pos="1"] { transform: translateX(calc(100% + 3rem)) rotateY(-50deg); opacity: 0; pointer-events: none; }
}
@media (prefers-reduced-motion: reduce) {
	.coverflow .coverflow-slide,
	.coverflow .coverflow-slide::after,
	.coverflow-dots-strip,
	.coverflow-dots--window .coverflow-dot::before { transition: none; }
}

/* Homepage blog teasers — same gradient-cover recipe as the case-card
   above (same three gradients, reused on purpose) so the two "featured
   work" and "from the blog" sections read as one visual family, just at
   a smaller scale since these are teasers into a full archive, not the
   proof-heavy case-card with its stats row. */
.blog-teaser-card { display: flex; flex-direction: column; background: var(--card); border: 1px solid var(--border); border-radius: var(--radius); padding: 1.5rem; transition: transform .35s var(--ease), box-shadow .35s; position: relative; overflow: hidden; }
.blog-teaser-card:hover { transform: translateY(-0.3125rem) !important; box-shadow: var(--shadow-soft); }
.blog-teaser-media { position: relative; aspect-ratio: 16 / 6.3; margin: -1.5rem -1.5rem 1.125rem; overflow: hidden; }
.blog-teaser-media .cover-bg { display: block; width: 100%; height: 100%; }
.blog-teaser-icon { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; }
.blog-teaser-icon svg { width: 2.125rem; height: 2.125rem; color: #fff; opacity: .92; filter: drop-shadow(0 2px 8px rgba(0,0,0,.25)); }
.blog-teaser-card h3 { font-size: 1.05rem; margin-bottom: 0.5rem; }
/* 2rem under the excerpt, not 1rem: the card was a touch short for its
   cover band, and on the card whose title wraps to two lines "Read Blog"
   sat only 16px under the text. This is the gap the height comes from, so
   the cover, padding and type stay exactly as they were. */
.blog-teaser-card p.excerpt { font-size: .9rem; color: var(--muted-foreground); margin-bottom: 2rem; }
/* Same "pin the CTA to the bottom" fix as .case-card — title/excerpt length
   varies card to card, so anchoring the link's own margin instead of a
   sibling block above it is enough here since there's no stats row to keep
   grouped with it. */
.blog-teaser-card .card-link { margin-top: auto; }

/* ==========================================================================
   Testimonials
   ========================================================================== */
.testimonial-card { background: var(--card); border: 1px solid var(--border); border-radius: var(--radius); padding: 1.875rem; display: flex; flex-direction: column; height: 100%; transition: transform .35s var(--ease), box-shadow .35s; }
.testimonial-card:hover { transform: translateY(-0.375rem) !important; box-shadow: var(--shadow-soft); }

/* ==========================================================================
   CTA band — closes on the hero's own ink, so the page ends in the colour
   it opened in before handing off to the footer.
   ========================================================================== */
/* Sits one step above the last stacked card (z 29) so it slides over the
   night card the same way the cards slide over each other. Left static it
   would pass underneath, since a sticky card with a z-index paints above a
   static sibling regardless of DOM order. */
.cta-band { position: relative; z-index: 30; padding: 6.25rem 0; background: radial-gradient(120% 160% at 50% -10%, rgba(93,160,172,.4), transparent 55%), linear-gradient(135deg, var(--hero-ink) 0%, var(--hero-ink-raise) 100%); color: #EAF3F1; text-align: center; }
.cta-band h2 { font-size: clamp(2rem, 3.5vw, 3.1rem); color: #EAF3F1; }
.cta-band p { margin: 1.25rem auto 2.25rem; max-width: 35rem; font-size: 1.1rem; opacity: .8; }
.cta-band .hero-cta { justify-content: center; }
.cta-band .btn-primary { background: var(--teal); color: var(--hero-ink); box-shadow: none; }
.cta-band .btn-outline { background: transparent; border-color: rgba(234,243,241,.35); color: #EAF3F1; }
/* The three promises under the button. In a monospace face a word space is
   a full character wide, so blank space alone doesn't read as a break between
   them: a small dot separates them, drawn rather than typed so screen readers
   don't announce it. No promise ever breaks mid-phrase; on phones, where all
   three can't share a line, they stack instead. */
.cta-meta { display: flex; flex-wrap: wrap; justify-content: center; gap: .375rem .75rem; margin-top: 1.75rem; font-family: var(--font-mono); font-size: .82rem; opacity: .7; }
.cta-meta li { display: flex; align-items: center; gap: .75rem; white-space: nowrap; }
.cta-meta li + li::before { content: ''; flex: none; width: 3px; height: 3px; border-radius: 50%; background: currentColor; }
@media (max-width: 30em) {
	.cta-meta { flex-direction: column; align-items: center; }
	.cta-meta li + li::before { display: none; }
}

/* ==========================================================================
   Footer
   ========================================================================== */
.site-footer { padding: 5rem 0 2rem; }
.footer-top { display: grid; grid-template-columns: 1.4fr repeat(3, 1fr); gap: 2rem; margin-bottom: 3.5rem; }
.footer-brand p { color: var(--muted-foreground); margin: 1rem 0 1.25rem; font-size: .9rem; }
.footer-socials { display: flex; gap: 0.625rem; }
.footer-socials a { width: 2.25rem; height: 2.25rem; border-radius: 50%; border: 1px solid var(--border); display: flex; align-items: center; justify-content: center; transition: border-color .2s, background .2s; }
.footer-socials a:hover { border-color: var(--teal); background: rgba(93,160,172,.12); }
.footer-socials svg { width: 1rem; height: 1rem; }
.footer-col h5 { font-family: var(--font-mono); font-size: .78rem; letter-spacing: .1em; text-transform: uppercase; margin-bottom: 1.125rem; font-weight: 500; }
.footer-col li { margin-bottom: 0.75rem; }
.footer-col a { color: var(--muted-foreground); font-size: .9rem; }
.footer-col a:hover { color: var(--foreground); }

/* Sitewide. The footer logo's size is the shared --logo-h, set with the
   header's near the top of this file; this is the other homepage-first
   change, the Company column's padding-left. Services/Company/Industries are
   equal-width (1fr) grid columns, so the 32px grid gap between them is
   geometrically identical -- but each column's *list* only fills as
   much of its box as its longest line needs, and that varies a lot:
   Services' long items (e.g. "Quantitative Analytics Testing") leave
   ~57px of trailing space in its box before the gap, while Company's
   short ones ("About", "Blog") leave ~171px. Visually that read as
   Services-to-Company: 89px, Company-to-Industries: 203px -- more than
   double -- which made Industries look like it started a new section
   rather than being the third of three equal columns (measured at
   1440px, where the 1280px container is already at its cap). Shifting
   Company's own content right by 57px equalises both to ~146px without
   moving either grid track. Tried on the homepage alone first (see git
   history); replicated sitewide once approved. */
.footer-top > .footer-col:nth-child(3) { padding-left: 3.5625rem; }
/* Awards — sits under the Industries links, in the column the removed
   Newsletter section used to occupy (no newsletter feature sitewide any
   more). The border-top is new for this move: under a form, the form
   itself was enough of a visual break before the badges; directly under a
   plain link list it needs its own hairline to read as a second thing
   inside the column rather than a stray image trailing the links. Nothing
   to click, so the image earns its place by standing out instead: the
   cobalt/persimmon glow (the site's signature highlight colours),
   redone as drop-shadow so it follows the badges'
   own transparent silhouette rather than boxing the image's rectangular
   canvas. */
.footer-awards { margin-top: 1.75rem; padding-top: 1.75rem; border-top: 1px solid var(--border); }
.footer-awards img {
	display: block; width: 100%; max-width: 11.25rem; height: auto;
	filter: drop-shadow(0 14px 30px rgba(43,52,224,.32)) drop-shadow(0 8px 20px rgba(255,106,69,.26));
}
.footer-bottom { display: flex; justify-content: space-between; align-items: center; padding-top: 1.75rem; border-top: 1px solid var(--border); font-size: .85rem; color: var(--muted-foreground); flex-wrap: wrap; gap: 1rem; }
.footer-bottom .legal-links { display: flex; gap: 1.25rem; }

/* ==========================================================================
   Inner page hero (non-homepage)
   ========================================================================== */
.page-hero { padding: calc(4.5rem + 5.25rem) 0 4rem; text-align: center; border-bottom: 1px solid var(--border); }
.page-hero h1 { font-size: clamp(2.2rem, 4vw, 3.2rem); }
.page-hero p { margin: 1.25rem auto 0; max-width: 40rem; font-size: 1.1rem; color: var(--muted-foreground); }

/* ==========================================================================
   Services hub hero — /services/ isn't a practice page, so it doesn't get
   a family hero (no rail color owns it, nothing to argue a single
   operational discipline for). Same live-canvas anatomy as every practice
   hero for visual consistency walking in from this page, but deliberately
   the plainest version of it: no split, no hero-visual, no stats, no CTA
   button — a canvas and the page's own existing tagline, centred, exactly
   as the brief asked. Text is unchanged from the old .page-hero's copy,
   just re-hosted. See docs/design/services-hub-hero-canvas-prompt.md for
   the canvas brief and why it's deliberately the one hero on this site
   with no discrete-event mechanism at all.

   The Company hub (/company/) wears this same hero -- same classes, same
   canvas script (a page-local copy, own canvas id) -- so the two hub pages
   read as siblings. It was a plain .page-hero before, which counted the
   fixed header's clearance twice; see .industry-hero below.

   The Contact page (/contact/) wears it too, the same way (own canvas
   id). Every "Talk to us" button on the site leads there, so it opens
   in the dark register of the pages those buttons sit on instead of a
   bare paper strip. */
.services-hero {
	--background: var(--hero-ink); --foreground: #EAF3F1; --surface: var(--hero-ink);
	--card: var(--hero-ink-raise); --muted-foreground: var(--lite-72);
	--accent: rgba(43,52,224,.16); --border: rgba(234,243,241,.14);
	background: var(--background); color: var(--foreground);
	position: relative; overflow: hidden; text-align: center;
	padding: 9.5rem 0 6rem;
}
.services-hero::before {
	content: ''; position: absolute; inset: -20% -10% auto -10%; height: 40rem; pointer-events: none;
	background: var(--gradient-aurora); opacity: .5; filter: blur(48px);
	background-size: 160% 160%; animation: aurora-drift 24s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) { .services-hero::before { animation: none; } }
.services-hero.is-live-network::before { display: none; }
#services-hero-aurora-bg, #company-hero-aurora-bg, #contact-hero-aurora-bg, #apply-hero-aurora-bg { position: absolute; inset: 0; z-index: 0; pointer-events: none; display: block; }
/* Radial, not the asymmetric left-weighted scrim every practice hero
   uses — those protect a left-aligned copy column; this hero's copy is
   centred, so the darkest point sits under the text and fades evenly
   toward all four edges instead of favouring one side. */
.services-hero.is-live-network::after {
	content: ''; position: absolute; inset: 0; z-index: 1; pointer-events: none;
	background: radial-gradient(60% 85% at 50% 42%, rgba(12,36,44,.82) 0%, rgba(12,36,44,.5) 55%, rgba(12,36,44,.08) 100%);
}
.services-hero .container { position: relative; z-index: 2; }
.services-hero-copy { max-width: 40rem; margin: 0 auto; }
.services-hero .eyebrow { color: var(--teal); justify-content: center; }
.services-hero h1 { font-size: clamp(2.4rem, 4.6vw, 3.6rem); color: #fff; }
.services-hero .svc-hero-copy { margin: 1.125rem 0 0; font-size: 1.15rem; color: var(--lite-72); }
@media (max-width: 53.75em) { /* 860px */
	/* 64px was less than the fixed header's own ~103px height, tucking the
	   eyebrow badge under the header's glass pill. 135px = header height
	   (103px) + a 32px breathing gap -- the fixed header doesn't shrink
	   with viewport width, so this one value holds from 860px down to the
	   smallest phone. */
	.services-hero { padding: 8.4375rem 0 4.5rem; }
}
.breadcrumb { font-size: .85rem; color: var(--muted-foreground); margin-bottom: 1.25rem; font-family: var(--font-mono); }
.breadcrumb a:hover { color: var(--foreground); }

/* Compensate for the fixed header everywhere except the homepage, where the
   hero stage runs full-bleed under it. */
#primary { padding-top: 5.25rem; }
.home #primary { padding-top: 0; }

.tech-pills { display: flex; flex-wrap: wrap; gap: 0.625rem; margin: 0 0 1.25rem; }
.tech-pills span { padding: 0.375rem 0.875rem; border-radius: 999px; background: var(--accent); border: 1px solid var(--border); font-size: .82rem; font-weight: 600; color: var(--foreground); }

.outcome-list { list-style: none; margin: 0 0 1.25rem; display: flex; flex-direction: column; gap: 0.75rem; }
.outcome-list li { display: flex; align-items: flex-start; gap: 0.625rem; margin-bottom: 0; }
.outcome-list svg { flex: none; width: 1.125rem; height: 1.125rem; margin-top: 0.1875rem; color: var(--teal-deep); }

/* ==========================================================================
   Service detail pages (/services/<practice>/) — these were riding the
   blog post template (.blog-layout/.blog-rail/.blog-prose) for its
   working scroll-spy rail, which is fine for the reading column but gives
   the page nothing to look at: no imagery, no proof, no sense of a real
   engagement. These three pieces are meant to be reusable across all 7
   service pages, piloted here on Agentic AI — a hero visual slot, a
   sourced-proof band, and a phase rail for whichever section describes a
   sequential engagement flow.
   ========================================================================== */
.svc-hero-copy { font-size: 1.06rem; line-height: 1.7; color: var(--ink-70); }
.svc-hero-copy strong { color: var(--ink); font-weight: 600; }

/* Re-declares the dark token set locally (same trick .card--lead and
   .hero use) so a .card-run dropped inside reads correctly even though
   this page never enters a dark section — the run panel is the point of
   this page, not a homepage-only component. */
.svc-hero-visual {
	--background: var(--ink); --foreground: #EAF3F1; --surface: var(--ink);
	--card: var(--ink-raise);
	--popover: rgba(255,255,255,.05); --muted: rgba(255,255,255,.07);
	--muted-foreground: var(--lite-72); --accent: rgba(43,52,224,.18);
	--accent-foreground: var(--beam); --border: rgba(234,243,241,.14);
	background: var(--ink); border-radius: var(--radius-lg); padding: 0.625rem;
	box-shadow: var(--shadow-soft);
}
.svc-hero-visual .card-run { background: rgba(4,10,14,.6); }

/* Photo variant, piloted on Cloud Engineering — the run panel stays exactly
   the same reused component, just floating at its natural size over a
   background photo instead of stretching to fill a flat ink frame. The
   gradient is asymmetric (darker where the panel sits, lighter toward the
   frame's outer edge) so the photo actually reads instead of disappearing
   under a flat wash, while the panel keeps the same contrast it has on
   every other service page. --hero-photo is set per-page via inline style
   since the image is page-specific, not a reusable token. */
.svc-hero-visual--photo {
	display: flex; align-items: center; padding: 1.75rem; min-height: 28.75rem;
	background-image: linear-gradient(115deg, rgba(9,14,20,.90) 6%, rgba(9,14,20,.62) 42%, rgba(9,14,20,.22) 100%), var(--hero-photo);
	background-size: cover; background-position: center;
}
.svc-hero-visual--photo .card-run {
	max-width: 23.75rem; background: rgba(4,10,14,.76);
	backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
	border-color: rgba(234,243,241,.16);
}
/* Same photo-legibility treatment as .card-run above, extended to .rag-panel
   now that AI Native Product Engineering floats it over a photo too — .rag-panel
   was previously only ever used on the flat --ink fill, which doesn't need
   the extra opacity/blur a busy photo does. */
.svc-hero-visual--photo .rag-panel {
	max-width: 23.75rem; background: rgba(4,10,14,.76);
	backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
	border-color: rgba(234,243,241,.16);
}
/* Same treatment again for .recon-panel, now that Business Intelligence
   floats it over a photo too — it was previously only ever used on the
   flat --ink fill. .recon-panel runs taller than .card-run/.rag-panel (two
   stacked "before"/"after" stages plus a model row), so it keeps the same
   opacity/blur but without their fixed max-width — clipping it to 380px
   here made the after-stage rows crowd. */
.svc-hero-visual--photo .recon-panel {
	align-self: stretch; background: rgba(4,10,14,.76);
	backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
	border-color: rgba(234,243,241,.16);
}
/* Same treatment again for .migration-ticket, now that BI Migration floats
   it over a photo too. */
.svc-hero-visual--photo .migration-ticket {
	max-width: 23.75rem; background: rgba(4,10,14,.76);
	backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
	border-color: rgba(234,243,241,.16);
}
/* Opt-in modifier for a photo brighter than the default scrim was tuned
   for (Cloud Engineering's isometric render, Gen AI's dark hands-on-desk
   shot — the scrim's default .90/.62/.22 stops assume that kind of
   source). Quality Engineering's hero photo is light gray with a blurred
   building exterior, and the default gradient's light trailing stop let
   too much of that brightness through. A page-level opt-in, not a
   .qa-hero-scoped rule, because brightness is a property of the photo a
   given page happens to use, not of the family — Quantitative Analytics
   Testing's own hero photo is already dark and reads fine under the
   default. */
.svc-hero-visual--photo-dim {
	background-image: linear-gradient(115deg, rgba(9,14,20,.94) 6%, rgba(9,14,20,.76) 42%, rgba(9,14,20,.58) 100%), var(--hero-photo);
}

/* ==========================================================================
   Cloud Engineering hero — restructured to match the Generative AI hero's
   anatomy (breadcrumb -> eyebrow -> h1 -> lede -> hero-cta -> hero-stats,
   live-canvas background, .split grid with a hero-visual on the right),
   replacing this page's earlier plain .page-hero + .svc-hero pair. Same
   token re-scope and live-network technique as .genai-hero — the canvas
   script at the bottom of the page is the same node/edge/aurora system,
   just pointed at a different canvas id, so the two hero backgrounds are
   deliberately siblings, not lookalikes drawn twice from scratch. The
   photo + Deploy · checkout-svc panel inside .hero-visual are untouched;
   this only changes what wraps around them. */
.cloud-hero {
	--background: var(--hero-ink); --foreground: #EAF3F1; --surface: var(--hero-ink);
	--card: var(--hero-ink-raise); --muted-foreground: var(--lite-72);
	--accent: rgba(43,52,224,.16); --border: rgba(234,243,241,.14);
	background: var(--background); color: var(--foreground);
	position: relative; overflow: hidden;
	padding: 9.5rem 0 5.25rem;
}
.cloud-hero::before {
	content: ''; position: absolute; inset: -20% -10% auto -10%; height: 40rem; pointer-events: none;
	background: var(--gradient-aurora); opacity: .5; filter: blur(48px);
	background-size: 160% 160%; animation: aurora-drift 24s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) { .cloud-hero::before { animation: none; } }
.cloud-hero.is-live-network::before { display: none; }
#cloud-hero-network-bg { position: absolute; inset: 0; z-index: 0; pointer-events: none; display: block; }
/* Scrim for the fleet canvas (see docs/design/cloud-engineering-hero-canvas-prompt.md)
   — the small-card version baked this into the card's own background-image;
   here the card is one small opaque element floating over a full-section
   canvas, so the copy column needs its own protection instead. Darkest
   behind the text, fading out by the point the fleet grid is meant to read
   clearly on its own. */
.cloud-hero.is-live-network::after {
	content: ''; position: absolute; inset: 0; z-index: 1; pointer-events: none;
	background: linear-gradient(100deg, rgba(12,36,44,.92) 0%, rgba(12,36,44,.78) 38%, rgba(12,36,44,0) 72%);
}
.cloud-hero .container { position: relative; z-index: 2; }
.cloud-hero .split { grid-template-columns: 1.02fr .98fr; }
.cloud-hero .breadcrumb a:hover { color: #fff; }
.cloud-hero .eyebrow { color: var(--teal); }
.cloud-hero h1 { font-size: clamp(2.25rem, 4.3vw, 3.4rem); max-width: 15ch; }
.cloud-hero .svc-hero-copy { margin-top: 1.375rem; max-width: 52ch; color: var(--lite-72); }
.cloud-hero .hero-cta { margin-top: 2rem; }
@media (max-width: 61.25em) { /* 980px */
	.cloud-hero .split { grid-template-columns: 1fr; gap: 2.5rem; }
}
@media (max-width: 53.75em) { /* 860px */
	/* 135px = fixed header height (~103px) + 32px gap -- see .services-hero's
	   860px block for why. Same fix, same shared header, across this whole
	   hero family. */
	.cloud-hero { padding: 8.4375rem 0 4.25rem; }
}

/* ==========================================================================
   AI Native Product Engineering hero — same restructuring as Cloud
   Engineering (breadcrumb -> eyebrow -> h1 -> lede -> hero-cta -> hero-stats,
   live-canvas background, .split grid with a hero-visual on the right),
   replacing this page's earlier plain .page-hero + .svc-hero pair. Same
   token re-scope and live-network technique as .cloud-hero/.genai-hero —
   the canvas script at the bottom of the page draws its own "review line"
   system (see the script's own comment for why it isn't a reskin of either
   sibling). The PR panel inside .hero-visual is untouched, just moved from
   the flat --ink fill into the --photo variant Cloud Engineering piloted;
   this only changes what wraps around it. */
.ainative-hero {
	--background: var(--hero-ink); --foreground: #EAF3F1; --surface: var(--hero-ink);
	--card: var(--hero-ink-raise); --muted-foreground: var(--lite-72);
	--accent: rgba(43,52,224,.16); --border: rgba(234,243,241,.14);
	background: var(--background); color: var(--foreground);
	position: relative; overflow: hidden;
	padding: 9.5rem 0 5.25rem;
}
.ainative-hero::before {
	content: ''; position: absolute; inset: -20% -10% auto -10%; height: 40rem; pointer-events: none;
	background: var(--gradient-aurora); opacity: .5; filter: blur(48px);
	background-size: 160% 160%; animation: aurora-drift 24s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) { .ainative-hero::before { animation: none; } }
.ainative-hero.is-live-network::before { display: none; }
#ainative-hero-network-bg { position: absolute; inset: 0; z-index: 0; pointer-events: none; display: block; }
/* Scrim for the review-line canvas — same asymmetric wash as Cloud
   Engineering's, darkest behind the copy column and fading out by where
   the lane field is meant to read clearly on its own. */
.ainative-hero.is-live-network::after {
	content: ''; position: absolute; inset: 0; z-index: 1; pointer-events: none;
	background: linear-gradient(100deg, rgba(12,36,44,.92) 0%, rgba(12,36,44,.78) 38%, rgba(12,36,44,0) 72%);
}
.ainative-hero .container { position: relative; z-index: 2; }
.ainative-hero .split { grid-template-columns: 1.02fr .98fr; }
.ainative-hero .breadcrumb a:hover { color: #fff; }
.ainative-hero .eyebrow { color: var(--teal); }
.ainative-hero h1 { font-size: clamp(2.25rem, 4.3vw, 3.4rem); max-width: 15ch; }
.ainative-hero .svc-hero-copy { margin-top: 1.375rem; max-width: 52ch; color: var(--lite-72); }
.ainative-hero .hero-cta { margin-top: 2rem; }

/* Tool strip — takes the slot .hero-stats used to hold on this hero, and
   inherits its exact 46px top margin so the copy column's rhythm is
   unchanged. Both stats that lived here (3M+, 99.9%) are restated word for
   word by the .svc-proof band a screen-width below, so the slot was echoing
   rather than adding: every sibling service hero (Generative AI, Agentic AI,
   Quality Engineering, Quant) carries at least one stat the band does not
   repeat, and this one carried none. The named tools are the page's whole
   differentiator — and the argument for them is already in the lede — so
   they take the slot. (The band and the hero's case-study button were
   removed on 2026-10-21 at the owner's request, so the page now cites no
   client result, the same as Cloud, Quant Testing and Staff Augmentation.
   Business Intelligence's hero uses this tool strip too.)

   Pill styling is deliberately the same language as .tech-row .tags span on
   the homepage (999px radius, hairline border, .85rem) so the two read as
   the same family. Like the homepage's, these sit still — the review-line
   canvas and the aurora wash are already moving behind this. The light
   translucent fill assumes the dark token scope every service hero sets up
   top (--border, --foreground re-pointed for ink); it is not a general
   light-surface component. */
.hero-tools { margin-top: 2.875rem; }
.hero-tools-label {
	display: block; font-family: var(--font-mono); font-size: var(--text-min); font-weight: 600;
	letter-spacing: .1em; text-transform: uppercase; color: var(--muted-foreground);
	margin-bottom: 0.8125rem;
}
.hero-tools-list { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 0.5rem; }
.hero-tools-list li {
	padding: 0.4375rem 0.875rem; border-radius: 999px; font-size: .85rem; color: var(--foreground);
	background: rgba(234,243,241,.07); border: 1px solid var(--border);
}
@media (max-width: 61.25em) { /* 980px */
	.ainative-hero .split { grid-template-columns: 1fr; gap: 2.5rem; }
}
@media (max-width: 53.75em) { /* 860px */
	.ainative-hero { padding: 8.4375rem 0 4.25rem; }
}

/* ==========================================================================
   Business Intelligence hero — same restructuring again (breadcrumb ->
   eyebrow -> h1 -> lede -> hero-cta -> live-canvas background, .split grid
   with a hero-visual on the right), replacing this page's earlier plain
   .page-hero + .svc-hero pair. No .hero-stats here — kept deliberately
   spare even now that real proof exists: BI Migration's "11 BI platforms
   supported" lives in the .svc-proof band directly below the hero instead
   (.svc-status-note's old "no case study yet" placeholder is gone — that
   component stays defined for a genuinely new future practice line, just
   unused here now). A product-adoption figure is a different kind of
   claim than the client-outcome stat AI Native/Cloud Engineering lead
   their heroes with, so it stays out of the hero rather than blurring
   into that pattern. Same reasoning is why hero-cta only ever has the one
   button — no "Read the case study" link to nothing. The reconciliation
   panel inside .hero-visual is
   untouched, just moved from the flat --ink fill into the --photo variant;
   this only changes what wraps around it. */
.bi-hero {
	--background: var(--hero-ink); --foreground: #EAF3F1; --surface: var(--hero-ink);
	--card: var(--hero-ink-raise); --muted-foreground: var(--lite-72);
	--accent: rgba(43,52,224,.16); --border: rgba(234,243,241,.14);
	background: var(--background); color: var(--foreground);
	position: relative; overflow: hidden;
	padding: 9.5rem 0 5.25rem;
}
.bi-hero::before {
	content: ''; position: absolute; inset: -20% -10% auto -10%; height: 40rem; pointer-events: none;
	background: var(--gradient-aurora); opacity: .5; filter: blur(48px);
	background-size: 160% 160%; animation: aurora-drift 24s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) { .bi-hero::before { animation: none; } }
.bi-hero.is-live-network::before { display: none; }
#bi-hero-network-bg { position: absolute; inset: 0; z-index: 0; pointer-events: none; display: block; }
/* Scrim for the convergence canvas — same asymmetric wash as the other two
   live-canvas heroes, darkest behind the copy column and fading out toward
   the visual side. */
.bi-hero.is-live-network::after {
	content: ''; position: absolute; inset: 0; z-index: 1; pointer-events: none;
	background: linear-gradient(100deg, rgba(12,36,44,.92) 0%, rgba(12,36,44,.78) 38%, rgba(12,36,44,0) 72%);
}
.bi-hero .container { position: relative; z-index: 2; }
.bi-hero .split { grid-template-columns: 1.02fr .98fr; }
.bi-hero .breadcrumb a:hover { color: #fff; }
.bi-hero .eyebrow { color: var(--teal); }
.bi-hero h1 { font-size: clamp(2.25rem, 4.3vw, 3.4rem); max-width: 15ch; }
.bi-hero .svc-hero-copy { margin-top: 1.375rem; max-width: 52ch; color: var(--lite-72); }
.bi-hero .hero-cta { margin-top: 2rem; }
@media (max-width: 61.25em) { /* 980px */
	.bi-hero .split { grid-template-columns: 1fr; gap: 2.5rem; }
	/* Stacked, the copy spans the full width (.hero-visual is hidden at this
	   same breakpoint), so the wash above -- clear from 72% to leave the
	   visual side open -- left the right third of the paragraph over the
	   canvas at full strength. Same angle and stops, but it holds .62 to the
	   edge instead of clearing. The canvas drops to its copy-column density
	   at this width too (see the page script). Together they measure what
	   the desktop copy column measures: contrast >= 6:1 and ~0.1% of pixels
	   changing per second behind the paragraph, against 1.5:1 and ~20%
	   before. */
	.bi-hero.is-live-network::after {
		background: linear-gradient(100deg, rgba(12,36,44,.92) 0%, rgba(12,36,44,.78) 38%, rgba(12,36,44,.62) 100%);
	}
}
@media (max-width: 53.75em) { /* 860px */
	.bi-hero { padding: 8.4375rem 0 4.25rem; }
}

/* ==========================================================================
   Quality Engineering hero — same restructuring as the Engineering-family
   heroes (breadcrumb -> eyebrow -> h1 -> lede -> hero-cta -> hero-stats,
   live-canvas background, .split grid with a hero-visual on the right),
   pilot for the Quality Assurance family (see
   docs/design/quality-assurance-family-design.md). Shared across both QA
   pages as one class rather than duplicated per page — the two pages get
   an identical hero treatment by design, so one selector is more honest
   than two copies that would only ever be edited in lockstep. The
   .card-run panel inside .hero-visual is untouched, just moved into the
   --photo variant already piloted on Cloud Engineering, and it gains one
   new element, .qa-verdict — see that rule below and the family spec's
   "one new microcopy line" section. Eyebrow deliberately uses --butter
   instead of the teal every Engineering-family hero uses: this family's
   whole argument is that a check produced a verdict, and butter is the
   token that carries that argument everywhere else on the page (the
   canvas, the checklist rail, the verdict line), so the hero's own
   eyebrow should carry it too rather than default to teal out of habit. */
.qa-hero {
	--background: var(--hero-ink); --foreground: #EAF3F1; --surface: var(--hero-ink);
	--card: var(--hero-ink-raise); --muted-foreground: var(--lite-72);
	--accent: rgba(43,52,224,.16); --border: rgba(234,243,241,.14);
	background: var(--background); color: var(--foreground);
	position: relative; overflow: hidden;
	padding: 9.5rem 0 5.25rem;
}
.qa-hero::before {
	content: ''; position: absolute; inset: -20% -10% auto -10%; height: 40rem; pointer-events: none;
	background: var(--gradient-aurora); opacity: .5; filter: blur(48px);
	background-size: 160% 160%; animation: aurora-drift 24s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) { .qa-hero::before { animation: none; } }
.qa-hero.is-live-network::before { display: none; }
#qa-hero-checks-bg { position: absolute; inset: 0; z-index: 0; pointer-events: none; display: block; }
/* Scrim for the check-field canvas — same asymmetric wash as every other
   live-canvas hero, darkest behind the copy column and fading out toward
   the visual side (see docs/design/quality-engineering-hero-canvas-prompt.md). */
.qa-hero.is-live-network::after {
	content: ''; position: absolute; inset: 0; z-index: 1; pointer-events: none;
	background: linear-gradient(100deg, rgba(12,36,44,.92) 0%, rgba(12,36,44,.78) 38%, rgba(12,36,44,0) 72%);
}
.qa-hero .container { position: relative; z-index: 2; }
.qa-hero .split { grid-template-columns: 1.02fr .98fr; }
.qa-hero .breadcrumb a:hover { color: #fff; }
.qa-hero .eyebrow { color: var(--butter); }
.qa-hero h1 { font-size: clamp(2.25rem, 4.3vw, 3.4rem); max-width: 15ch; }
.qa-hero .svc-hero-copy { margin-top: 1.375rem; max-width: 52ch; color: var(--lite-72); }
.qa-hero .hero-cta { margin-top: 2rem; }
@media (max-width: 61.25em) { /* 980px */
	.qa-hero .split { grid-template-columns: 1fr; gap: 2.5rem; }
}
@media (max-width: 53.75em) { /* 860px */
	.qa-hero { padding: 8.4375rem 0 4.25rem; }
}

/* Verdict line — the one new element .card-run gains for this family
   (Quantitative Analytics Testing's .rag-panel already ends on an
   equivalent line; this makes both hero visuals end on the same beat).
   Sits directly under .run-foot, same family accent as everywhere else
   this page uses --butter for "resolved". */
.qa-verdict {
	display: flex; align-items: center; gap: 0.4375rem; margin-top: 0.625rem; padding-top: 0.625rem;
	border-top: 1px solid rgba(234,243,241,.14);
	font-size: .82rem; font-weight: 600; color: var(--butter);
}
.qa-verdict svg { flex: none; color: var(--butter); }

/* ==========================================================================
   BI Migration hero — same restructuring as every other live-canvas hero
   (breadcrumb -> eyebrow -> h1 -> lede -> hero-cta -> hero-stats, live-
   canvas background, .split grid with a hero-visual on the right),
   replacing this page's earlier plain .page-hero + full-width static SVG.
   See docs/design/bi-migration-hero-canvas-prompt.md for "the changeover"
   canvas concept — six named source docks, six named target docks, a
   three-stage gate in between, reports that accumulate rather than loop
   or fan back out. */
.bimig-hero {
	--background: var(--hero-ink); --foreground: #EAF3F1; --surface: var(--hero-ink);
	--card: var(--hero-ink-raise); --muted-foreground: var(--lite-72);
	--accent: rgba(43,52,224,.16); --border: rgba(234,243,241,.14);
	background: var(--background); color: var(--foreground);
	position: relative; overflow: hidden;
	padding: 9.5rem 0 5.25rem;
}
.bimig-hero::before {
	content: ''; position: absolute; inset: -20% -10% auto -10%; height: 40rem; pointer-events: none;
	background: var(--gradient-aurora); opacity: .5; filter: blur(48px);
	background-size: 160% 160%; animation: aurora-drift 24s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) { .bimig-hero::before { animation: none; } }
.bimig-hero.is-live-network::before { display: none; }
#bimig-hero-changeover-bg { position: absolute; inset: 0; z-index: 0; pointer-events: none; display: block; }
/* Scrim for the changeover canvas — same asymmetric wash as every other
   live-canvas hero, darkest behind the copy column and fading out toward
   the visual side. */
.bimig-hero.is-live-network::after {
	content: ''; position: absolute; inset: 0; z-index: 1; pointer-events: none;
	background: linear-gradient(100deg, rgba(12,36,44,.92) 0%, rgba(12,36,44,.78) 38%, rgba(12,36,44,0) 72%);
}
.bimig-hero .container { position: relative; z-index: 2; }
.bimig-hero .split { grid-template-columns: 1.02fr .98fr; }
.bimig-hero .breadcrumb a:hover { color: #fff; }
.bimig-hero .eyebrow { color: var(--teal); }
.bimig-hero h1 { font-size: clamp(2.25rem, 4.3vw, 3.4rem); max-width: 15ch; }
.bimig-hero .svc-hero-copy { margin-top: 1.375rem; max-width: 52ch; color: var(--lite-72); }
.bimig-hero .hero-cta { margin-top: 2rem; }
.bimig-hero .hero-stats { grid-template-columns: repeat(2, auto); margin-top: 2.875rem; }
.bimig-hero .hero-stats div strong { color: #fff; }
.bimig-hero .hero-stats div span { color: var(--lite-72); }
@media (max-width: 61.25em) { /* 980px */
	.bimig-hero .split { grid-template-columns: 1fr; gap: 2.5rem; }
}
@media (max-width: 53.75em) { /* 860px */
	.bimig-hero { padding: 8.4375rem 0 4.25rem; }
}
/* The hero's own 84px bottom padding is the hero's, not a gap — the very
   next element (a plain heading, unlike the boxed note Business
   Intelligence once had in the same spot) started 0px below the hero's edge
   and read as glued to it. Scoped to the element right after this
   specific hero rather than a global .section-head margin, since that
   class is shared with two other sections further down this same page. */
.bimig-hero + .container { padding-top: 4rem; }
/* Same problem one section further down, but not the same fix. "What
   you get" is the second of two bare .container siblings between the
   hero and the first .section — its "What you get" eyebrow started 0px
   below the pipeline's closing note and read as glued to it. But 64px
   above is a compensating value, not a general-purpose gap: the hero
   already contributes its own 84px bottom padding, so that transition
   totals 148px, deliberately larger right after the hero's heavy visual
   break. This transition has no such contribution from the pipeline's
   side (its closing note carries no bottom padding), so reusing 64px
   here would total only 64px — smaller than both the transition before
   it (148px) and the one right after it into "Before you commit" (96px,
   native to .section). 96px matches that steady-state rhythm instead,
   so the page reads as one deliberate gap after the hero, then a
   consistent one between every plain section from here on. Chained off
   the same selector as above for the same reason: a global .section-head
   margin would double up on the two .section-wrapped sections further
   down. */
.bimig-hero + .container + .container { padding-top: 6rem; }

/* Migration ticket — BI Migration's own hero-visual signature, modeled on
   .recon-panel's visual language (same border/radius/mono-font/opacity
   scale) but its own markup: one concrete report's live Parse / Translate
   / Validate status, the same three-stage gate the canvas draws in bulk
   behind it — a concrete instance next to the ambient aggregate, the same
   relationship .recon-panel has to Business Intelligence's own canvas.
   Illustrative, not a claimed real client report — no company name, no
   dollar figure, same discipline as Cloud Engineering's "checkout-svc". */
.migration-ticket {
	border: 1px solid var(--border); border-radius: 0.75rem;
	background: rgba(4,10,14,.5); padding: 1.375rem; font-family: var(--font-mono);
	color: var(--lite-72); font-size: 0.75rem;
}
.ticket-head {
	display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 0.25rem 0.625rem;
	padding-bottom: 0.8125rem; border-bottom: 1px solid var(--border);
	font-size: 0.78125rem; color: #EAF3F1; font-weight: 500;
}
.ticket-route { font-size: var(--text-min); letter-spacing: .04em; color: var(--teal); white-space: nowrap; }
.ticket-steps { margin-top: 1rem; display: flex; flex-direction: column; gap: 0.3125rem; }
.ticket-step {
	display: flex; align-items: flex-start; gap: 0.6875rem; padding: 0.5625rem 0.625rem;
	border-radius: 0.4375rem; background: rgba(234,243,241,.04);
}
.ticket-step-dot {
	flex: none; width: 0.5625rem; height: 0.5625rem; border-radius: 50%; margin-top: 0.1875rem;
	border: 1.5px solid rgba(234,243,241,.32); background: transparent;
}
.ticket-step.is-done .ticket-step-dot { background: #7BE0A8; border-color: #7BE0A8; }
.ticket-step.is-current .ticket-step-dot {
	background: var(--butter); border-color: var(--butter);
	box-shadow: 0 0 0 3px rgba(245,206,91,.25);
}
.ticket-step b { display: block; font-size: var(--text-min); color: #EAF3F1; font-weight: 600; }
.ticket-step span { display: block; margin-top: 2px; font-size: var(--text-min); color: var(--lite-45); line-height: 1.4; }
.ticket-foot {
	display: flex; align-items: center; gap: 0.4375rem; margin-top: 0.875rem; padding-top: 0.8125rem;
	border-top: 1px solid var(--border); font-size: var(--text-min); color: #7BE0A8;
}
.ticket-foot svg { flex: none; }

/* Migration flow — replaces both the old static bi-migration-pipeline.svg
   and the separate .svc-phases card grid directly below it, which was
   drawing the exact same three-stage message twice (a diagram, then a
   near-identical set of cards restating it). One dark panel now carries
   both jobs: the diagram's own fill and framing, a real icon, title and
   full description per stage, connected by literal arrows so the
   sequence reads without three disconnected cards needing to imply it.
   Numbering was dropped on purpose — an arrow between each stage already
   says "this order, this direction"; a numeral on top of that is a
   second signal for the same fact, not new information. Icon per stage:
   Lucide file-search / arrow-right-left / shield-check (ISC license,
   sourced from lucide.dev, cited in docs/design/image-credits.md). */
.migration-flow {
	max-width: 65rem; margin: 0.5rem auto 0; padding: 3rem 2.75rem 2.5rem;
	background: #101619; border-radius: var(--radius-lg);
	border: 1px solid rgba(234,243,241,.1); box-shadow: var(--shadow-soft);
	display: grid; grid-template-columns: 1fr auto 1fr auto 1fr; align-items: start; gap: 0 1.25rem;
}
.migration-flow-icon {
	display: flex; align-items: center; justify-content: center;
	width: 3.25rem; height: 3.25rem; border-radius: 50%; flex: none;
	background: color-mix(in srgb, var(--stage-accent) 14%, transparent);
	border: 1.5px solid var(--stage-accent); color: var(--stage-accent);
	margin-bottom: 1.25rem;
}
.migration-flow-stage h3 { color: #fff; font-size: 1.05rem; margin: 0 0 0.5625rem; }
.migration-flow-stage p { color: var(--lite-72); font-size: .92rem; line-height: 1.6; margin: 0; }
.migration-flow-arrow {
	display: flex; align-items: center; justify-content: center;
	margin-top: 0.9375rem; color: rgba(234,243,241,.24);
}
@media (max-width: 53.75em) { /* 860px */
	.migration-flow { grid-template-columns: 1fr; gap: 1.75rem 0; padding: 2.25rem 1.625rem; }
	.migration-flow-arrow { display: none; }
}

/* Platform Analysis Suite — a tinted band with white cards, each card
   opening on a small dark "screen". It was the third of four near-
   identical light sections in a row, so the middle of the page read as
   one long grey scroll. Tinted rather than dark: a dark band in the
   hero's teal read as a second hero (and --hero-ink is documented as hero
   only), and these cards explain tools, which the site keeps in daylight;
   dark is for proof. The band is the industry pages' own tone
   (body.ind-tonal's --band), and on it the eyebrow takes --teal-ink,
   since --teal-deep drops to ~3.6:1 there.
   The screens are --ink, the same dark as the pipeline card above, and
   show what each tool does without words: a lens over a report stack
   (Insight Analyzer), a file dropped into a tray becoming a breakdown
   sheet (Cost Estimator), a prompt and a spreadsheet becoming a chart
   (AI Visualization Assistant). Shapes only: no numbers, names or sample
   values, since the page has no real data to show. The top margin keeps
   a paper gap after the bare "What you get" container, which carries no
   bottom padding of its own. */
.bimig-suite { margin-top: 6rem; background: #D8E6E7; }
.bimig-suite .eyebrow { color: var(--teal-ink); }
.bimig-tool-visual {
	margin: 0 0 1.5rem; aspect-ratio: 32 / 15; border-radius: 0.75rem; overflow: hidden;
	background: var(--ink); border: 1px solid var(--ink);
}
.bimig-tool-visual svg { display: block; width: 100%; height: 100%; }

/* "Parity, not approximation" — the six outcomes on the left, and beside
   them the claim made visible: the same report in the old tool (grey) and
   in the new one (teal), with every part drawn at identical size and
   position, and a tick linking each matching pair. Wordless and without
   values, like the tool screens below. It replaces the list's two-column
   grid, which left a wide empty gap under six short lines. Light rather
   than dark: the pipeline card above and the tool screens below are
   already the page's dark moments. */
.bimig-parity { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.1fr); gap: 3rem; align-items: center; }
.bimig-parity .outcome-list { margin: 0; }
.bimig-parity-card {
	background: var(--card); border: 1px solid var(--border); border-radius: var(--radius);
	padding: 1.25rem; box-shadow: var(--shadow-soft);
}
.bimig-parity-card svg { display: block; width: 100%; height: auto; }
@media (max-width: 53.75em) { /* 860px */
	.bimig-parity { grid-template-columns: minmax(0, 1fr); gap: 2rem; }
	.bimig-parity-card { max-width: 36rem; width: 100%; justify-self: center; }
}

/* Platform interchange — replaces the old plain-text .tech-row "Moving
   from" / "Moving to" lists, which stated the same fact set the hero's
   own canvas and ticket panel already argue (named source platforms,
   named target platforms) in a flatter, unillustrated form. Same
   five-part rhythm as .migration-flow above it (list -> arrow -> center
   -> arrow -> list) so the two sections read as one continued idea
   rather than two unrelated components. Deliberately NOT a 6x6 pairwise
   matrix — see docs/design/bi-migration-design.md for why: the page has
   never claimed every source/target pair is equally deep, and a
   checkbox grid would be a new claim this component isn't positioned to
   back. Logos render in native brand color on a plain white tile, never
   recolored — see docs/design/image-credits.md for the sourcing and the
   reasoning (IBM's own brand guidelines are explicit that their logo
   "shouldn't be altered in any way," and the same caution was extended
   to every mark here, not just IBM's). */
.platform-interchange {
	display: grid; grid-template-columns: 1fr auto auto auto 1fr;
	align-items: center; gap: 0 1.25rem; margin-bottom: 1.375rem;
}
.interchange-label {
	display: block; font-family: var(--font-mono); font-size: var(--text-min); font-weight: 600;
	letter-spacing: .1em; text-transform: uppercase; color: var(--muted-foreground);
	margin-bottom: 1rem;
}
.interchange-col--to .interchange-label { text-align: right; }
.interchange-logos { display: grid; grid-template-columns: 1fr 1fr; gap: 0.625rem; }
.interchange-logo {
	display: flex; align-items: center; gap: 0.625rem; padding: 0.625rem 0.75rem;
	background: var(--card); border: 1px solid var(--border); border-radius: 0.625rem;
	font-size: .82rem; font-weight: 500; color: var(--foreground);
}
.interchange-logo img { flex: none; width: 1.625rem; height: 1.625rem; object-fit: contain; }
.interchange-logo small { display: block; font-size: var(--text-min-sans); font-weight: 400; color: var(--muted-foreground); }
.interchange-arrow { display: flex; align-items: center; justify-content: center; color: var(--border); }
.interchange-engine { display: flex; flex-direction: column; align-items: center; text-align: center; gap: 0.5rem; }
.interchange-engine-badge {
	display: flex; align-items: center; justify-content: center;
	width: 3.5rem; height: 3.5rem; border-radius: 50%; flex: none;
	background: var(--accent); border: 1.5px solid var(--teal-deep); color: var(--teal-deep);
}
.interchange-engine b { font-size: .92rem; color: var(--foreground); }
.interchange-engine span {
	display: block; font-family: var(--font-mono); font-size: var(--text-min); letter-spacing: .05em;
	color: var(--muted-foreground); white-space: nowrap;
}
.interchange-trademark {
	max-width: 40rem; margin: 0 auto 3.5rem; text-align: center;
	font-size: var(--text-min-sans); color: var(--muted-foreground); line-height: 1.6;
}
@media (max-width: 61.25em) { /* 980px */
	.platform-interchange { grid-template-columns: 1fr; gap: 1.875rem 0; }
	.interchange-arrow { display: none; }
	.interchange-col--to .interchange-label { text-align: left; }
}
@media (max-width: 30em) { /* 480px */
	.interchange-logos { grid-template-columns: 1fr; }
}

/* Data foundation — the reframed "Reads from" row. The migration
   changes the BI layer and re-points connections; it doesn't touch the
   warehouse underneath — the most reassuring true thing on this page
   for a data team, previously rendered as a neutral list of nouns
   indistinguishable from the two logo rows above it. A visually
   distinct band (dashed top rule, no logos, centered) rather than a
   third identical .tech-row, so the reassurance reads as structural,
   not just another item in the same list shape. */
.data-foundation {
	border-top: 1px dashed var(--border); padding-top: 2.5rem; text-align: center;
}
.data-foundation-label {
	display: inline-flex; align-items: center; gap: 0.5rem;
	font-family: var(--font-mono); font-size: var(--text-min); font-weight: 600;
	letter-spacing: .1em; text-transform: uppercase; color: var(--teal-deep); margin-bottom: 0.75rem;
}
.data-foundation-label svg { flex: none; }
.data-foundation p { max-width: 35rem; margin: 0 auto 1.375rem; color: var(--muted-foreground); font-size: .96rem; }
.data-foundation .tags { display: flex; flex-wrap: wrap; justify-content: center; gap: 0.5rem; }
.data-foundation .tags span {
	padding: 0.4375rem 0.8125rem; border-radius: 999px; background: var(--card); border: 1px solid var(--border); font-size: .85rem;
}

/* Checklist rail — the Quality Assurance family's in-page nav, distinct
   from both .blog-rail (circular dot, teal, a timeline) and .svc-rail (one
   sliding bar, no per-item state): each item is its own checkbox, empty
   while its section is ahead, outlined while current, filled with a tick
   once read past — "what's been checked," not "where am I." Ticked by
   initQaChecklist() in main.js, same IntersectionObserver mechanism as
   initBlogLog(), reusing its is-read/is-current classes so the two rails
   share one JS pattern even though they render differently. */
.blog-layout.has-qa-rail { grid-template-columns: 13.75rem minmax(0, 1fr); padding-bottom: 4rem; }
.has-qa-rail .blog-column { max-width: none; }
.qa-checklist { position: sticky; top: 6.5rem; }
.qa-checklist-label {
	font-size: var(--text-min); letter-spacing: .09em; text-transform: uppercase;
	color: var(--ink-70); opacity: .55; margin-bottom: 1.125rem;
}
.qa-checklist ol { list-style: none; margin: 0; padding: 0; }
.qa-checklist-item { position: relative; padding: 0 0 1.25rem 1.625rem; }
.qa-checklist-item:last-child { padding-bottom: 0; }
.qa-checklist-item a {
	display: block; font-size: .92rem; line-height: 1.4; font-weight: 500;
	color: var(--ink-70); transition: color .2s ease, font-weight .2s ease;
}
.qa-checklist-item::before {
	content: ''; position: absolute; left: 0; top: 2px; width: 0.875rem; height: 0.875rem;
	border-radius: 0.25rem; border: 1.5px solid rgba(16,22,25,.3);
	background: transparent; transition: background .2s ease, border-color .2s ease;
}
.qa-checklist-item::after {
	content: '\2713'; position: absolute; left: 0; top: 2px; width: 0.875rem; height: 0.875rem;
	font-size: 0.625rem; line-height: 0.875rem; text-align: center; color: transparent;
	transition: color .15s ease;
}
.qa-checklist-item.is-current::before { border-color: var(--butter); box-shadow: 0 0 0 3px rgba(245,206,91,.18); }
.qa-checklist-item.is-current a { color: var(--ink); font-weight: 700; }
.qa-checklist-item.is-read::before { background: var(--butter); border-color: var(--butter); }
.qa-checklist-item.is-read::after { color: var(--hero-ink); }
.qa-checklist-item a:hover { color: var(--teal-deep); }

/* Check-list bullets — the content-block differentiator for this family:
   no per-topic imagery exists (unlike .svc-feature's sourced photos), so
   the only visual change from plain .blog-prose is this bullet, a small
   tick instead of the default dash, keeping "checklist" language running
   from the rail into the copy itself.

   Drawn at the size and with the glyph of the ticks on .outcome-list and
   .svc-feature-list, the other service pages' bullets: 18px, the same
   polyline, a 10px gap to the text. It used to be a 7x4px border-drawn
   check with a hairline stroke, which on the pale topic tint read as a
   speck beside them. A mask rather than an inline <svg> in every <li>, so
   the markup stays plain list items and the colour is still the --butter
   token (the pattern .roster's icons use). */
.qa-check-list { --qa-tick: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'/%3E%3C/svg%3E"); margin: 0 0 1.125rem 0.25rem; padding: 0; }
.qa-check-list > li { position: relative; padding-left: 1.75rem; margin-bottom: 0.5625rem; line-height: 1.65; list-style: none; }
.qa-check-list > li::before {
	content: ''; position: absolute; left: 0; top: .28em; width: 1.125rem; height: 1.125rem;
	background: var(--butter);
	-webkit-mask: var(--qa-tick) center / contain no-repeat;
	mask: var(--qa-tick) center / contain no-repeat;
}
@media (max-width: 45em) { /* 720px */
	/* Above the content, the rail stays the same single checklist it is
	   beside it. It was set in two CSS columns, which balance by height:
	   with three sections the split flipped between 2+1 and 1+2 as labels
	   wrapped in the ~150px columns, leaving a hole under one side. And
	   margin: 0 auto on a grid item shrink-wraps and centres it, so the
	   list sat 9px (430px phone) to 154px (720px) right of the cards'
	   left edge. No auto margins: it stretches to the column like the
	   cards below it. */
	.blog-layout.has-qa-rail { grid-template-columns: minmax(0, 1fr); }
	.qa-checklist { position: static; margin: 0 0 0.5rem; }
}

/* ==========================================================================
   Topic card — the Quality Assurance family's content-block shape, once
   real per-topic imagery existed to build one (see the "why text-only"
   reasoning in docs/design/quality-assurance-family-design.md — this
   supersedes it now that the user supplied real sourced images per
   section). Alternates sides row to row via .reverse, same `order`-swap
   technique as .capability, not .svc-feature's fixed image-right layout —
   this family's version zigzags. Each instance sets --topic-tint and
   --topic-accent inline (per-image palette, sourced from that section's
   own photo — see the credits doc), the same "page-specific, so inline
   not a token" pattern --hero-photo already uses. Deliberately a *light*
   surface, unlike the dark hero — "light theme, innovate" was the brief,
   and it also reads as the inverse of the dark hero: the hero is the
   check running, these cards are the evidence room afterward. */
.qa-topic {
	display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); align-items: start;
	border-radius: var(--radius-lg); overflow: hidden; background: var(--topic-tint);
}
/* Each card sits inside its own .blog-section (the checklist rail's scroll
   anchor), not as a direct sibling of the next card, so the extra gap a
   tile this visually heavy wants is scoped to .has-qa-rail's own
   .blog-section rather than touching the sitewide 52px blog-post rhythm. */
.has-qa-rail .blog-section { margin-top: 2.25rem; }
.has-qa-rail .blog-section:first-of-type { margin-top: 0; }
.qa-topic.reverse .qa-topic-media { order: 2; }
.qa-topic.reverse .qa-topic-copy { order: 1; }
/* Fixed, not stretched to match the copy column: copy runs anywhere from
   ~630px to ~890px depending on how many bullets a topic has, and
   stretching a landscape source photo to fill that would crop away most
   of it. 420px keeps every image a sane, still-photographic crop; the
   card's own tint (identical to the fade's destination color) continues
   underneath in whatever the image doesn't fill, so a shorter image
   never reads as an unfinished box, just a photo pinned to the top of a
   tinted panel. */
.qa-topic-media { position: relative; height: 26.25rem; }
.qa-topic-media img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block; }
/* The "no hard stop" fade — the image's own trailing edge (the side
   nearest the text) eases into --topic-tint, the exact color the copy
   panel starts on, so the seam between the two grid cells disappears
   instead of reading as a cut line. Direction flips with .reverse since
   "trailing edge" is whichever side faces the text. */
.qa-topic-media::after {
	content: ''; position: absolute; inset: 0;
	background: linear-gradient(to right, transparent 55%, var(--topic-tint) 100%);
}
.qa-topic.reverse .qa-topic-media::after {
	background: linear-gradient(to left, transparent 55%, var(--topic-tint) 100%);
}
.qa-topic-copy { padding: 2.875rem 3rem; display: flex; flex-direction: column; justify-content: center; }
.qa-topic-copy::before {
	content: ''; display: block; width: 2.125rem; height: 0.1875rem; border-radius: 2px;
	background: var(--topic-accent); margin-bottom: 1.125rem;
}
.qa-topic-h2 { font-family: var(--font-display); font-size: 1.6rem; margin: 0 0 0.875rem; color: var(--ink); }
.qa-topic-lead { font-size: 1rem; line-height: 1.65; color: var(--ink-70); margin: 0 0 1.375rem; }
@media (max-width: 53.75em) { /* 860px */
	.qa-topic-copy { padding: 2.25rem 2rem; }
}
@media (max-width: 45em) { /* 720px */
	.qa-topic, .qa-topic.reverse { grid-template-columns: 1fr; }
	/* Media-first, not the h2->lead->visual->list split .capability/
	   .svc-feature--illus use -- reverted after review. These photos read
	   as generic stock imagery rather than a specific diagram, so
	   sandwiching one between the lead and the list read as a long, empty
	   scroll in the middle of the argument. Better as a cover image ahead
	   of the text than a break in the middle of it. */
	.qa-topic .qa-topic-media, .qa-topic.reverse .qa-topic-media { order: 1; min-height: 13.75rem; }
	.qa-topic .qa-topic-copy, .qa-topic.reverse .qa-topic-copy { order: 2; }
	/* Stacked, the fade now needs to run top -> bottom into the copy panel
	   below it, not left/right into a column that no longer exists beside it. */
	.qa-topic-media::after, .qa-topic.reverse .qa-topic-media::after {
		background: linear-gradient(to bottom, transparent 55%, var(--topic-tint) 100%);
	}
}

/* ==========================================================================
   Teams hero — the family's own hero, not a reskin of the other three.
   Same restructuring anatomy (breadcrumb -> eyebrow -> h1 -> lede ->
   hero-cta -> hero-stats, live-canvas background, .split grid with a
   hero-visual on the right) because that anatomy is genuinely sitewide
   furniture now, not something to differentiate on. Eyebrow uses
   --teal-deep, this family's own mega-menu rail color, the same way QA's
   hero uses --butter. .roster (the existing bench-of-engineers visual) is
   carried into .hero-visual completely unchanged — an asset, not a
   placeholder — sitting in the plain (non-photo) .svc-hero-visual card,
   since there's no photo to embed behind it here. Only two hero-stats:
   the two real numbers this page has (350+ engineers, 24 countries), both
   already the page's own proof figures — no invented third stat to round
   out a row. No .svc-proof band below the hero either: with no case study
   to link to, a second band restating the same two numbers would be
   pure duplication, not added proof. */
.teams-hero {
	--background: var(--hero-ink); --foreground: #EAF3F1; --surface: var(--hero-ink);
	--card: var(--hero-ink-raise); --muted-foreground: var(--lite-72);
	--accent: rgba(43,52,224,.16); --border: rgba(234,243,241,.14);
	background: var(--background); color: var(--foreground);
	position: relative; overflow: hidden;
	padding: 9.5rem 0 5.25rem;
}
.teams-hero::before {
	content: ''; position: absolute; inset: -20% -10% auto -10%; height: 40rem; pointer-events: none;
	background: var(--gradient-aurora); opacity: .5; filter: blur(48px);
	background-size: 160% 160%; animation: aurora-drift 24s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) { .teams-hero::before { animation: none; } }
.teams-hero.is-live-network::before { display: none; }
.teams-hero.is-live-network::after {
	content: ''; position: absolute; inset: 0; z-index: 1; pointer-events: none;
	background: linear-gradient(100deg, rgba(12,36,44,.92) 0%, rgba(12,36,44,.78) 38%, rgba(12,36,44,0) 72%);
}
.teams-hero .container { position: relative; z-index: 2; }
.teams-hero .split { grid-template-columns: 1.02fr .98fr; }
.teams-hero .breadcrumb a:hover { color: #fff; }
.teams-hero .eyebrow { color: var(--teal-deep); }
.teams-hero h1 { font-size: clamp(2.25rem, 4.3vw, 3.4rem); max-width: 15ch; }
.teams-hero .svc-hero-copy { margin-top: 1.375rem; max-width: 54ch; color: var(--lite-72); }
.teams-hero .hero-cta { margin-top: 2rem; }
.teams-hero .hero-stats { grid-template-columns: repeat(2, auto); margin-top: 2.875rem; }
.teams-hero .hero-stats div strong { color: #fff; }
.teams-hero .hero-stats div span { color: var(--lite-72); }
.teams-hero .svc-hero-visual { --border: rgba(234,243,241,.14); }
@media (max-width: 61.25em) { /* 980px */
	.teams-hero .split { grid-template-columns: 1fr; gap: 2.5rem; }
}
@media (max-width: 53.75em) { /* 860px */
	.teams-hero { padding: 8.4375rem 0 4.25rem; }
}

/* The promise — deliberately its own banded section, not a bare list
   sitting in the page's default background between the dark hero and the
   white Core Competencies section. A distinct --muted fill plus real
   top/bottom padding gives it a floor and ceiling of its own, so it reads
   as "the promise, stated plainly" rather than an afterthought still
   attached to the hero above it. */
.teams-promise { padding: 5.5rem 0; background: var(--muted); }
.teams-promise-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1.25rem; }
.teams-promise-card {
	display: flex; align-items: flex-start; gap: 1rem;
	padding: 1.625rem 1.75rem; background: var(--paper); border-radius: var(--radius);
}
.teams-promise-icon {
	display: flex; align-items: center; justify-content: center; flex: none;
	width: 2.125rem; height: 2.125rem; border-radius: 50%; background: rgba(47,126,142,.14); color: var(--teal-deep);
}
.teams-promise-card p { margin: 0; padding-top: 0.3125rem; font-size: .96rem; line-height: 1.55; color: var(--ink-70); }
@media (max-width: 45em) { /* 720px */
	.teams-promise { padding: 3.5rem 0; }
	.teams-promise-grid { grid-template-columns: 1fr; }
}

/* Core competencies — the reference layout's own device: a vertical spine
   down the center connecting four alternating image/copy rows, each row's
   own node sitting on the line. Distinct from .qa-topic (no spine, no
   node, flat alternation) and from .capability (accent color per row
   carries the meaning, not a connecting line) — the spine is specifically
   right here because "core competencies" reads as one continuous
   discipline, not four independent, browsable topics the way QA's testing
   disciplines or Engineering's practices are. */
.teams-competency-list { position: relative; display: flex; flex-direction: column; gap: 3.5rem; padding: 0.5rem 0; }
.teams-competency-spine {
	position: absolute; left: 50%; top: 0; bottom: 0; width: 2px; transform: translateX(-50%);
	background: linear-gradient(to bottom, transparent, var(--hair-l) 6%, var(--hair-l) 94%, transparent);
	pointer-events: none;
}
.teams-competency {
	position: relative; display: grid; grid-template-columns: 1fr 1fr; gap: 4rem; align-items: center;
}
.teams-competency.reverse .teams-competency-media { order: 2; }
.teams-competency.reverse .teams-competency-copy { order: 1; }
.teams-competency-media {
	border-radius: var(--radius-lg); overflow: hidden; aspect-ratio: 4 / 3; box-shadow: var(--shadow-soft);
}
.teams-competency-media img { width: 100%; height: 100%; object-fit: cover; display: block; }
.teams-competency-node {
	position: absolute; left: 50%; top: 50%; width: 0.9375rem; height: 0.9375rem; border-radius: 50%;
	transform: translate(-50%, -50%); background: var(--paper); border: 2.5px solid var(--teal-deep);
	box-shadow: 0 0 0 6px var(--paper);
	transition: background .35s ease, box-shadow .35s ease;
}
/* Active node — filled solid instead of paper-with-outline, plus a soft
   halo. Ticked by initTeamsCompetency() in main.js (a scroll-driven
   current check, not IntersectionObserver — see that function's own
   comment for why). The node's own box only ever transitions color/
   shadow now; it does not scale or move. An earlier pass animated the
   node itself (a scale "pop"), which read as a jerky bounce rather than
   smooth, because scaling the element that also holds the border/shadow
   forces layout-adjacent recalculation every frame. */
.teams-competency-node.is-current {
	background: var(--teal-deep);
	box-shadow: 0 0 0 6px var(--paper), 0 0 0 11px rgba(47,126,142,.22);
}
/* The ripple is a separate ring (::after), not the node's own box, so it
   can expand and fade continuously while the filled node underneath stays
   perfectly still. transform + opacity only — no box-shadow spread or
   width/height animation — so it stays GPU-composited and smooth rather
   than forcing a layout recalculation every frame. */
.teams-competency-node.is-current::after {
	content: ''; position: absolute; inset: -0.15625rem; border-radius: 50%;
	background: var(--teal-deep);
	animation: teams-node-ripple 1.8s ease-out infinite;
	pointer-events: none;
}
@keyframes teams-node-ripple {
	0% { transform: scale(1); opacity: .5; }
	100% { transform: scale(2.8); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
	.teams-competency-node { transition: none; }
	.teams-competency-node.is-current::after { animation: none; content: none; }
}
.teams-competency-copy { max-width: 44ch; }
.teams-competency.reverse .teams-competency-copy { justify-self: end; text-align: right; }
.teams-competency-tag {
	display: block; font-family: var(--font-mono); font-size: var(--text-min); font-weight: 500;
	letter-spacing: .13em; text-transform: uppercase; color: var(--teal-deep); margin-bottom: 0.75rem;
}
.teams-competency-copy p { font-size: 1rem; line-height: 1.65; color: var(--ink-70); }
@media (max-width: 53.75em) { /* 860px */
	.teams-competency-list { gap: 2.5rem; }
	/* padding-left clears the spine/node (left: 24px below, a 15px circle
	   centered on that line) out of the content column. Harmless dead space
	   when media rendered first -- top: 0 landed on the photo's corner --
	   but copy renders first now (see the order swap below), and top: 0
	   lands squarely on the tag's own text without it. Applied to the row
	   itself, not just the copy, so text and photo keep sharing one left
	   edge instead of the photo staying flush while only the text indents. */
	.teams-competency, .teams-competency.reverse { grid-template-columns: 1fr; gap: 1.375rem; padding-left: 2.75rem; }
	/* Copy-first, not media-first -- unlike .capability/.svc-feature/
	   .qa-topic, .teams-competency-copy is just a tag + one paragraph, no
	   heading and no bullet list to interleave the visual between, so
	   there's nothing for display: contents to buy here. A plain order
	   swap (this was media: 1 / copy: 2) already gets the right result:
	   text first, visual after. */
	.teams-competency .teams-competency-media, .teams-competency.reverse .teams-competency-media { order: 2; }
	.teams-competency .teams-competency-copy, .teams-competency.reverse .teams-competency-copy {
		order: 1; max-width: none; justify-self: start; text-align: left;
	}
	.teams-competency-spine { left: 1.5rem; }
	.teams-competency-node { left: 1.5rem; top: 0; }
}

/* Why choose us — deliberately not another photo section (the page
   already spends its one photographic moment on Core Competencies). Three
   short reason cards plus a role-chip cluster that echoes .roster's own
   per-seat color rotation from the hero, rather than a stock "handshake"
   photo — the roles list is the actual proof of "a full bench," so it gets
   to be the visual, not an illustration standing in for it. */
.teams-why { padding-top: 0.5rem; }
.teams-why-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.625rem; margin-bottom: 3rem; }
.teams-why-card {
	padding: 2rem 1.875rem; background: var(--muted); border-radius: var(--radius);
}
.teams-why-mark { display: block; width: 2.125rem; height: 2.125rem; border-radius: 0.5625rem; background: var(--mark); margin-bottom: 1.25rem; opacity: .16; }
.teams-why-card h3 { font-size: 1.08rem; margin: 0 0 0.625rem; color: var(--ink); }
.teams-why-card p { font-size: .94rem; line-height: 1.6; color: var(--ink-70); margin: 0; }
.teams-roles {
	border-radius: var(--radius-lg); background: var(--ink); padding: 2.125rem 2.25rem; margin-bottom: 4.5rem;
}
/* Without this, .teams-roles (dark, rounded) runs straight into .cta-band
   (also dark, full-bleed) with no gap — two different dark surfaces
   touching read as a spacing bug, not a deliberate close, the same class
   of issue .blog-layout.has-svc-rail's own padding-bottom was added to
   fix on the Engineering pages. 72px matches that fix's own reasoning:
   enough room that the rounded card and the band beneath it are legibly
   two things, not scoped any wider than .teams-roles itself. */
.teams-roles-label {
	display: block; font-family: var(--font-mono); font-size: var(--text-min); font-weight: 500;
	letter-spacing: .13em; text-transform: uppercase; color: var(--lite-72); margin-bottom: 1.125rem;
}
.teams-roles-list { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 0.625rem; }
.teams-roles-list li {
	position: relative; padding: 0.5rem 1rem 0.5rem 1.625rem; border-radius: 6.25rem;
	background: rgba(234,243,241,.06); border: 1px solid rgba(234,243,241,.14);
	font-size: .86rem; color: #EAF3F1; font-weight: 500;
}
.teams-roles-list li::before {
	content: ''; position: absolute; left: 0.75rem; top: 50%; width: 0.4375rem; height: 0.4375rem;
	border-radius: 50%; transform: translateY(-50%); background: var(--chip);
}
@media (max-width: 53.75em) { /* 860px */
	.teams-why-grid { grid-template-columns: 1fr; gap: 1rem; }
}
@media (max-width: 35em) { /* 560px */
	.teams-roles { padding: 1.625rem 1.375rem; }
}

/* Proof band — one real, sourced result linking straight to its case
   study, framed the same honest way as the practice cards on /services/
   ("client result", not an implied universal average). */
.svc-proof {
	display: flex; align-items: center; gap: 1.5rem; flex-wrap: wrap;
	margin: 0 0 4rem; padding: 1.625rem 1.875rem; border-radius: var(--radius);
	background: var(--ink); color: #EAF3F1; transition: transform .3s var(--ease);
}
.svc-proof:hover { transform: translateY(-0.1875rem); }
.svc-proof-figure {
	font-family: var(--font-display); font-variation-settings: 'wdth' 106, 'wght' 740;
	font-size: clamp(1.8rem, 4vw, 2.1rem); letter-spacing: -.01em; flex: none;
}
.svc-proof-body { flex: 1; min-width: 13.75rem; }
.svc-proof-tag {
	display: block; font-family: var(--font-mono); font-size: var(--text-min); font-weight: 500;
	letter-spacing: .14em; text-transform: uppercase; color: var(--butter); margin-bottom: 0.375rem;
}
.svc-proof-label { font-size: .95rem; color: var(--lite-72); line-height: 1.5; }
.svc-proof-link { flex: none; display: inline-flex; align-items: center; gap: 0.375rem; font-size: .88rem; font-weight: 600; color: var(--teal); }
.svc-proof-link svg { transition: transform .3s var(--ease); }
.svc-proof:hover .svc-proof-link svg { transform: translateX(0.25rem); }

/* Proof band backdrop. A modifier, so the other pages that share .svc-proof
   (Agentic AI, Generative AI, AI Native Product Engineering and the rest)
   keep the flat ink; today only Quality Engineering and Business
   Intelligence carry it.

   Several faint lines settle onto one baseline that ends in a single node:
   many signals, one verified result, which is what the band claims. The
   light, the dot grid and the node sit at the bottom-right, the link's side,
   and everything fades out toward the figure on the left so the number
   still reads against plain ink.

   The band runs from a ~1230x105px strip on a laptop to a ~270x320px card
   on a phone, so nothing here assumes a size. The lines are one SVG
   stretched to the box (preserveAspectRatio="none"; the non-scaling strokes
   keep them 1px and the nodes round); the grid and the light are CSS
   gradients, so their pitch and falloff never distort. The SVG stops 10px
   short of the bottom edge, so the baseline always sits inside the bottom
   padding (22-26px): on the stacked phone layout it passes under the link
   rather than through it, and the end node's halo isn't clipped.

   Legibility was measured, not assumed: with the text hidden, the lightest
   backdrop pixel under each text box against that text's colour, on both
   pages at 8 widths from 320 to 1920px. The link is the tightest and stays
   at 4.6:1 or better (4.5 needed; the flat ink gave 6.2), the label at 4.8:1
   or better. That is why the lines stay very faint until they merge (on the
   stacked layout they cross the link), the dots bloom only around the
   corner, and the light is a flat pool along the bottom edge. Brighten any
   of those and re-measure the link first. */
.svc-proof--converge {
	background:
		url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1000 200' preserveAspectRatio='none'%3E%3Cstyle%3Epath{vector-effect:non-scaling-stroke;fill:none}%3C/style%3E%3Cdefs%3E%3ClinearGradient id='g' gradientUnits='userSpaceOnUse' x1='0' x2='1000'%3E%3Cstop offset='0' stop-color='%235DA0AC' stop-opacity='0'/%3E%3Cstop offset='.3' stop-color='%235DA0AC' stop-opacity='.03'/%3E%3Cstop offset='.78' stop-color='%235DA0AC' stop-opacity='.075'/%3E%3Cstop offset='.84' stop-color='%235DA0AC' stop-opacity='.3'/%3E%3Cstop offset='1' stop-color='%235DA0AC' stop-opacity='.42'/%3E%3C/linearGradient%3E%3ClinearGradient id='b' gradientUnits='userSpaceOnUse' x1='0' x2='1000'%3E%3Cstop offset='0' stop-color='%235DA0AC' stop-opacity='0'/%3E%3Cstop offset='.45' stop-color='%235DA0AC' stop-opacity='.1'/%3E%3Cstop offset='.84' stop-color='%235DA0AC' stop-opacity='.5'/%3E%3Cstop offset='1' stop-color='%235DA0AC' stop-opacity='.6'/%3E%3C/linearGradient%3E%3C/defs%3E%3Cg stroke='url(%23g)'%3E%3Cpath d='M0 14C360 14 560 199 840 199'/%3E%3Cpath d='M0 66C360 66 560 199 840 199'/%3E%3Cpath d='M0 118C360 118 560 199 840 199'/%3E%3Cpath d='M0 170C360 170 560 199 840 199'/%3E%3C/g%3E%3Cpath d='M0 199H1000' stroke='url(%23b)' stroke-width='1.5'/%3E%3Cpath d='M840 199h.01' stroke='%235DA0AC' stroke-width='9' stroke-opacity='0.2' stroke-linecap='round'/%3E%3Cpath d='M840 199h.01' stroke='%235DA0AC' stroke-width='3.5' stroke-linecap='round'/%3E%3Cpath d='M968 199h.01' stroke='%23F5CE5B' stroke-width='16' stroke-opacity='0.16' stroke-linecap='round'/%3E%3Cpath d='M968 199h.01' stroke='%23F5CE5B' stroke-width='5.5' stroke-linecap='round'/%3E%3C/svg%3E") 0 0 / 100% calc(100% - 10px) no-repeat,
		radial-gradient(ellipse 78% 56% at 100% 100%, rgba(93,160,172,.22), transparent),
		radial-gradient(ellipse 78% 78% at 100% 100%, transparent 6%, var(--ink) 50%),
		radial-gradient(circle at 8px 8px, rgba(234,243,241,.16) 1px, transparent 1.5px) 0 0 / 16px 16px,
		var(--ink);
}

/* Business Intelligence hero visual — the page's own claim ("every team
   has its own version of revenue") made literal: three source systems
   disagreeing, reconciled through one semantic layer into a number three
   different consumers all read the same way. Same fact set as the small
   .diagram-model motif on the homepage's BI card, expanded to hero scale
   with the actual "before" (the messy, disagreeing numbers) alongside the
   "after", not just the clean end state alone. Deliberately its own
   component rather than reusing .rag-panel — that's Generative AI's
   citation/retrieval device, and this page needs its own signature, not
   a reskin of another practice's. */
.recon-panel {
	border: 1px solid var(--border); border-radius: 0.75rem;
	background: rgba(4,10,14,.5); padding: 1.375rem; font-family: var(--font-mono);
	color: var(--lite-72); font-size: 0.75rem;
}
.recon-panel-head {
	display: flex; align-items: center; gap: 0.375rem; padding-bottom: 0.8125rem;
	border-bottom: 1px solid var(--border); font-size: var(--text-min); letter-spacing: .06em;
	color: rgba(234,243,241,.42);
}
.recon-panel-head svg { flex: none; }
.recon-stage { margin-top: 1rem; }
.recon-stage-label {
	display: block; font-size: var(--text-min); letter-spacing: .1em; text-transform: uppercase;
	color: rgba(234,243,241,.38); margin-bottom: 0.5rem;
}
.recon-row {
	display: flex; align-items: center; justify-content: space-between; gap: 0.625rem;
	padding: 0.5rem 0.625rem; border-radius: 0.4375rem; background: rgba(234,243,241,.04);
	margin-bottom: 0.3125rem; font-size: var(--text-min);
}
.recon-row:last-child { margin-bottom: 0; }
.recon-sys { color: var(--lite-72); }
.recon-val { color: var(--sun); font-weight: 600; letter-spacing: -.01em; }
.recon-row.is-ok { background: rgba(123,224,168,.08); }
.recon-row.is-ok .recon-val { color: #7BE0A8; }
.recon-model {
	display: flex; align-items: center; gap: 0.5rem; margin: 1rem 0; padding: 0.625rem 0.75rem;
	background: rgba(43,52,224,.16); border: 1px solid rgba(43,52,224,.32);
	border-radius: 0.5rem; color: #EAF3F1; font-size: var(--text-min);
}
.recon-model svg { flex: none; color: var(--beam); }
.recon-model b { color: #fff; }
.recon-foot {
	display: flex; align-items: center; gap: 0.4375rem; margin-top: 0.875rem; padding-top: 0.8125rem;
	border-top: 1px solid var(--border); font-size: var(--text-min); color: #7BE0A8;
}
.recon-foot svg { flex: none; }

/* Phase rail — turns a process mention that was buried in a sentence
   into something scannable. Three steps because that's what the copy
   actually describes; not a generic device dropped in for decoration. */
.svc-phases-head { max-width: 40rem; margin: 0 0 2rem; }
.svc-phases-head h2 { font-size: 1.3rem; margin-bottom: 0.625rem; }
.svc-phases-head p { color: var(--muted-foreground); font-size: .98rem; }
.svc-phases { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.75rem; margin-bottom: 0.5rem; position: relative; }
/* The circles sit left-aligned in each column (not centred, like the
   homepage's process-row), so the connecting line's endpoints are derived
   from the actual grid math rather than guessed percentages: circle 1's
   centre is always exactly half its own width from the left edge (21px),
   independent of container width; circle 3's centre falls at column 3's
   start (2 tracks + 2 gaps in) plus that same 21px. track = (100% - 2×28px)/3,
   so the right inset simplifies to (100% - 56px)/3 - 21px. A flat 14%/14%
   guess left a gap before circle 1 and ran past circle 3 at every width. */
.svc-phases::before {
	content: ''; position: absolute; top: 1.3125rem; height: 1px; background: var(--border); z-index: 0;
	left: 1.3125rem; right: calc((100% - 3.5rem) / 3 - 1.3125rem);
}
.svc-phase { position: relative; z-index: 1; }
.svc-phase-num {
	width: 2.625rem; height: 2.625rem; border-radius: 50%; background: var(--card); border: 1px solid var(--border);
	display: flex; align-items: center; justify-content: center; margin-bottom: 1.125rem;
	font-family: var(--font-display); font-variation-settings: 'wdth' 108, 'wght' 700; font-size: .95rem;
	color: var(--teal-deep);
}
.svc-phase h3 { font-size: 1.02rem; margin-bottom: 0.5rem; }
.svc-phase p { font-size: .9rem; color: var(--muted-foreground); line-height: 1.55; }

@media (max-width: 53.75em) { /* 860px */
	.svc-hero-visual { order: -1; }
	.svc-hero-visual--photo { min-height: 20rem; padding: 1.25rem; }
	.svc-hero-visual--photo .card-run { max-width: none; }
}
@media (max-width: 40em) { /* 640px */
	.svc-phases { grid-template-columns: 1fr; gap: 2rem; }
	.svc-phases::before { display: none; }
	.svc-proof { padding: 1.375rem; }
}

/* ==========================================================================
   Contact page
   ========================================================================== */
/* The two cards climb into the hero's bottom padding (--contact-lift), so
   the dark band hands over to the form instead of stopping on a flat line,
   and the first fields land higher on the screen. The hero keeps the
   Services hub's own padding. The grid sets its own z-index because the
   hero's scrim (1) and container (2) otherwise paint above anything after
   them: the lifted part of the cards would come out tinted. The cards stretch
   to the taller one so both end on the same line. */
.contact-section.section { padding-top: 0; }
.contact-grid {
	--contact-lift: 3rem;
	display: grid; grid-template-columns: 1fr 1fr; gap: 2rem; align-items: stretch;
	position: relative; z-index: 3; margin-top: calc(var(--contact-lift) * -1);
}
.contact-form { display: grid; gap: 1.125rem; }
.form-row { display: grid; gap: 0.5rem; }
.form-row label { font-size: .85rem; font-weight: 600; }
.form-row input, .form-row textarea, .form-row select {
	background: var(--card); border: 1px solid var(--border); border-radius: 0.625rem; padding: 0.75rem 1rem;
	color: var(--foreground); font-size: .95rem; width: 100%;
}
.form-row textarea { resize: vertical; min-height: 8.75rem; }
/* Attachment and spam-check fields. The file field's button is restyled as a
   pill so it matches the page's buttons; the sum's box is short because the
   answer is one or two digits. */
.contact-field-hint { margin: 0; font-size: .85rem; color: var(--muted-foreground); }
.contact-field-error { margin: 0; font-size: .88rem; color: var(--sun-deep); }
.contact-field-error[hidden] { display: none; }
.form-row input[type="file"] { padding: 0.5rem 0.625rem; font-size: .9rem; cursor: pointer; }
.form-row input[type="file"]::file-selector-button {
	margin-right: 0.75rem; padding: 0.375rem 0.875rem; border: 1px solid var(--border); border-radius: 999px;
	background: var(--card); color: var(--ink); font: inherit; font-weight: 600; cursor: pointer;
}
.form-row input[type="file"]::file-selector-button:hover { background: var(--muted); }
.contact-check input[type="text"] { max-width: 7rem; }
/* Both cards share one shell: white, hairline border, the site's own card
   shadow. The form's fields are tinted so they still read as fields on the
   white. */
.contact-form-card, .contact-sheet {
	background: var(--card); border: 1px solid var(--border); border-radius: var(--radius);
	box-shadow: var(--shadow-card);
}
.contact-form-card { padding: 2rem 2.125rem; }
.contact-form-card .form-row :is(input, textarea) { background: var(--background); }
/* Contact sheet -- one quiet panel beside the form card instead of three
   identical cards: both offices as real postal addresses with a way to
   get there. The form is the only way to write to us, so no email
   address lives here or anywhere else on the site. City names carry the
   only display type; everything else stays small so the form keeps the
   page's attention. No hover lift: nothing here is one big click
   target, so the old cards' lift promised an action that wasn't there. */
.contact-sheet { padding: 0.375rem 2.125rem; }
.contact-sheet-row { padding: 1.625rem 0; }
.contact-sheet-label {
	font-family: var(--font-sans); font-variation-settings: normal; letter-spacing: 0; line-height: 1.4;
	font-size: .85rem; font-weight: 600; color: var(--muted-foreground); margin-bottom: 0.5rem;
}
.contact-sheet-note { font-size: .85rem; color: var(--muted-foreground); margin-bottom: 1.25rem; }
.contact-office + .contact-office { margin-top: 1.75rem; }
.contact-office h3 { font-size: 1.25rem; margin-bottom: 0.5rem; }
.contact-office address { font-style: normal; font-size: .95rem; line-height: 1.6; color: var(--ink-70); }
.contact-office-map {
	display: inline-flex; align-items: center; gap: 0.375rem; margin-top: 0.625rem;
	font-size: .88rem; font-weight: 600; color: var(--teal-ink);
}
.contact-office-map:hover { color: var(--ink); text-decoration: underline; text-underline-offset: 0.1875rem; }
.contact-form-note { font-size: .88rem; color: var(--muted-foreground); }
@media (max-width: 53.75em) { /* 860px: the hero's bottom padding drops from 6rem to 4.5rem here, so the lift does too */
	.contact-grid { --contact-lift: 2rem; }
}
@media (max-width: 40em) { /* 640px */
	.contact-form-card { padding: 1.75rem 1.5rem; }
	.contact-sheet { padding: 0.25rem 1.5rem; }
}

/* Reads the ?sent=1 / ?sent=0 flag contact/send.php redirects back with —
   see initContactStatus() in main.js, which fills this in and reveals it.
   Reuses the brand's own teal (already the "good" signal elsewhere on the
   site) and persimmon (the site's one warm/attention accent) rather than
   introducing a generic red/green that doesn't exist in the palette. */
.contact-status { display: flex; align-items: flex-start; gap: 0.75rem; border-radius: var(--radius); padding: 1rem 1.25rem; margin-bottom: 1.125rem; font-size: .95rem; line-height: 1.5; }
.contact-status[hidden] { display: none; }
.contact-status svg { flex: none; width: 1.25rem; height: 1.25rem; margin-top: 2px; }
.contact-status--success { background: color-mix(in srgb, var(--teal) 12%, var(--card)); border: 1px solid color-mix(in srgb, var(--teal) 35%, var(--border)); color: var(--teal-ink); }
.contact-status--error { background: color-mix(in srgb, var(--sun) 10%, var(--card)); border: 1px solid color-mix(in srgb, var(--sun) 35%, var(--border)); color: var(--sun-deep); }

/* ==========================================================================
   Careers
   ========================================================================== */
.perk-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.5rem; }

/* ==========================================================================
   Blog
   ========================================================================== */
/* Was margin-top only, so the page numbers (the blog index's three pages)
   sat flush against the footer with no breathing room below them. */
.pagination { display: flex; gap: 0.625rem; justify-content: center; margin: 3rem 0 4rem; }
.pagination a, .pagination span { padding: 0.625rem 1rem; border-radius: 0.5rem; border: 1px solid var(--border); font-size: .9rem; }
.pagination .current { background: var(--accent); }

/* ==========================================================================
   Scroll reveal
   ========================================================================== */
.reveal {
	opacity: 0;
	transform: translateY(1.625rem);
	transition: opacity .7s var(--ease), transform .7s var(--ease);
}
.reveal.is-visible { opacity: 1; transform: translateY(0); }
@media (prefers-reduced-motion: reduce) {
	.reveal { opacity: 1; transform: none; transition: none; }
}

/* ==========================================================================
   Cursor glow
   ========================================================================== */
.cursor-glow {
	position: fixed; top: 0; left: 0; width: 13.75rem; height: 13.75rem; border-radius: 50%;
	pointer-events: none; z-index: 60;
	background: radial-gradient(circle, rgba(255,255,255,.3), transparent 4.1875rem);
	mix-blend-mode: difference;
	transform: translate(-12.5rem, -12.5rem) translate(-50%, -50%);
	will-change: transform;
}
@media (hover: none) {
	.cursor-glow { display: none; }
}

/* ==========================================================================
   Card hover spotlight
   ========================================================================== */
.card::before, .case-card::before, .testimonial-card::before, .blog-teaser-card::before {
	content: '';
	position: absolute;
	inset: -1px;
	border-radius: inherit;
	background: radial-gradient(17.5rem circle at var(--spot-x, 50%) var(--spot-y, 50%), rgba(93,160,172,.16), transparent 70%);
	opacity: 0;
	transition: opacity .3s ease;
	pointer-events: none;
}
.card:hover::before, .case-card:hover::before, .testimonial-card:hover::before, .blog-teaser-card:hover::before { opacity: 1; }

/* ==========================================================================
   Hero particles
   ========================================================================== */
.hero-particles { position: absolute; inset: 0; overflow: hidden; pointer-events: none; }
.hero-particles span {
	position: absolute;
	border-radius: 50%;
	animation: particle-float linear infinite;
}
@keyframes particle-float {
	0%, 100% { transform: translateY(0); opacity: .2; }
	50% { transform: translateY(-2.5rem); opacity: .8; }
}
@media (prefers-reduced-motion: reduce) {
	.hero-particles span { animation: none; opacity: .4; }
}

/* ==========================================================================
   Responsive
   ========================================================================== */
@media (max-width: 61.25em) { /* 980px */
	.grid-3, .grid-4 { grid-template-columns: repeat(2, 1fr); }
	/* .industries-strip (About Us "Where we go deepest") had no collapse
	   rule at all -- still 3 columns at phone width otherwise. Same
	   3 -> 2 -> 1 taper as .grid-3 just above, matching its own card
	   content (photo cover + heading + paragraph + link). Compound
	   selector (not just .industries-strip) because its own base rule
	   sits much later in this file, in the About Us section -- matching
	   specificity instead of depending on where that rule happens to be. */
	.grid.industries-strip { grid-template-columns: repeat(2, 1fr); }
	.hero .container, .split { grid-template-columns: 1fr; }
	/* Below this width the hero's visual is dropped, not re-stacked. It is
	   the right-hand column of a two-column hero -- the photo, panel or card
	   stack that supports the headline from the side -- and every hero family
	   collapses to a single column here (each one's own max-width: 980px
	   rule), which used to push it under the copy: a lone card or photo at the
	   very bottom of the hero, below the CTAs and stats, with nothing beside it
	   and a long scroll past the claim it was meant to support. (Headline-first
	   was already the mobile default for the same reason -- the visual
	   supports the claim, not the other way round -- before this took the
	   visual out of the stack altogether.) Scoped to the hero families rather
	   than the bare class because .hero-copy/.hero-visual are reused by split
	   sections further down some pages (Quantitative Finance's "Where AI
	   fits"), where the panel is part of that section's content and still
	   stacks under its copy. It takes the homepage's two floating cards
	   (.hero-code, .hero-approval) and its -67px desktop offset with it, so
	   neither needs a mobile rule of its own any more. Hiding is not enough
	   for a photo that is a real <img>: display: none doesn't stop its
	   download (a CSS background in a hidden subtree is never fetched, which
	   is why the service heroes need nothing), so those photos sit in a
	   <picture> whose (max-width: 980px) <source> is a 1x1 GIF. Keep that
	   media query in step with the 980px here. */
	:is(.hero, .photo-hero, .genai-hero, .cloud-hero, .bi-hero, .qa-hero, .ainative-hero, .teams-hero, .bimig-hero) .hero-visual { display: none; }
	.stat-row { grid-template-columns: repeat(2, 1fr); }
	/* 4 columns divide evenly into 2x2 now that Newsletter (and its
	   column) is gone — no odd column left dangling on its own row the
	   way a 5-into-3 split used to need. */
	.footer-top { grid-template-columns: repeat(2, 1fr); }
	/* Company's padding-left (see the comment above the base rule) only
	   makes sense sandwiched between Services and Industries in the
	   4-column row -- reset it here, where Company moves to its own
	   row-start in the 2x2 stack. */
	.footer-top > .footer-col:nth-child(3) { padding-left: 0; }
	.contact-grid { grid-template-columns: 1fr; gap: 1.5rem; }
	.perk-grid { grid-template-columns: repeat(2, 1fr); }
	/* Side by side, the two lead cards are already narrower than the old
	   single lead card was at full width, so each stacks its own content
	   (tag/heading/paragraph above, visual below) rather than splitting
	   into two columns internally — same reasoning that already applied
	   to the single lead card at this width, just always-on now. */
	.services-lead-row { grid-template-columns: 1fr; }
	/* Six equal-weight cards divide evenly into 2 columns / 3 rows — no
	   dangling last card to special-case now that Business Intelligence
	   makes it six instead of five. */
	.services-reg-row { grid-template-columns: repeat(2, 1fr); }
	/* Full card width only makes the waterfall truer — the ticks separate and
	   the hold reads at its real length, so nothing needs reproportioning. */
	.card-run { margin-top: 0.25rem; }
	.run-rows { padding: 1.375rem 0; }
}
@media (max-width: 45em) { /* 720px */
	.card--lead { padding: 1.75rem; }
	.services-reg-row { grid-template-columns: 1fr; }
	.card-run, .rag-panel { padding: 1.125rem; }
	.run-label { font-size: var(--text-min); }
	.run-rows { gap: 0.9375rem; }
}
@media (max-width: 47.5em) { /* 760px */
	:root { --shrink-x: .028; }
	.hero-stage { height: 109vh; }
	/* Shorter runway on a phone — three cards is already a fair bit of
	   scroll on a small screen. */
	.stack > .stack-item {
		margin-bottom: clamp(1.75rem, 6vh, 3.5rem);
		max-width: calc(100% - 1.5rem);
	}
}

/* Below 980px the hero's copy+visual stack into one column (see the
   max-width: 980px block above) -- together, routinely taller than the
   pinned stage's 100dvh window and taller than the ~100px of scroll the
   stage leaves to reveal it. That left the headline, CTAs and stats
   sitting invisible (.reveal's opacity: 0, above) well past a normal
   scroll -- sometimes past where the pinned stage even ends. Same
   fallback the reduced-motion block below already uses, for the same
   reason: drop the pin, let the hero sit in plain document flow at its
   natural height, and show its copy immediately instead of waiting on a
   reveal the pin no longer drives to bring it on screen. Deliberately a
   second max-width: 980px block rather than folded into the one above --
   this one also has to win over the narrower max-width: 760px block's own
   .hero-stage height, which only makes sense for the pinned effect this is
   turning off. initHeroStage() in main.js mirrors this with its own
   matchMedia check, so .is-day still toggles correctly here. */
@media (max-width: 61.25em) { /* 980px */
	.hero-stage { height: auto; }
	.hero-stage__sticky { position: static; height: auto; }
	.hero { clip-path: none !important; border-radius: 0 !important; box-shadow: none !important; height: auto; min-height: 100vh; padding: 5.625rem 0 4.375rem; }
	.hero .container { transform: none !important; }
	/* Overrides the vh-based clamp at .home .hero .container (main rule,
	   above the Responsive section) -- that clamp exists to fit the copy
	   column inside a strictly-100dvh-clipped hero on short desktop/laptop
	   screens, a constraint the scroll-pin disabling just above removes at
	   this width. Phones are tall in portrait, so the clamp's vh math was
	   landing at its 170px desktop ceiling here instead of shrinking, on
	   top of .hero's own 90px above -- 260px of dead air before the
	   eyebrow. 45px + .hero's 90px = 135px total, the same header-height-
	   plus-gap this hero family uses everywhere else (see .services-hero's
	   860px block). Equal selector specificity to the clamp rule, later in
	   source order, so this wins at this width without touching the
	   desktop-only clamp itself. --copy-shift is the bigger logo's
	   allowance (see the main rule); 0 on phones. */
	.home .hero .container { padding-top: calc(2.8125rem + var(--copy-shift)); }
	.hero-copy.reveal, .hero-visual.reveal { opacity: 1; transform: none; transition: none; }
	/* Was max-width: 720px, same as everything else below -- moved up to
	   match this block's own breakpoint instead. Logo + all 5 nav items +
	   dropdown arrows + "Talk to us" simply don't fit a header this width:
	   measured at 768px and 860px, the logo (a 216px-wide image at its
	   normal 36px height) was being flex-squeezed down to 28px and 120px
	   respectively to let the rest of the row fit without wrapping --
	   readable as "the logo looks squeezed" at best and as a barely-legible
	   sliver at worst. The row's true combined width doesn't fit until
	   ~980px, so that's where the hamburger needs to take over instead of
	   721px; the nav bar's own 720-1145px positioning caveat for its mega
	   panel (see positionMegaPanel in main.js) shrinks to a narrower, less
	   awkward range as a side effect. */
	.primary-nav { display: none; }
	.menu-toggle { display: inline-flex; }
}
/* Logo + "Talk to us" + the hamburger toggle are the only three things left
   in the header row below 720px (the full nav is gone above) -- and with
   .site-branding correctly excluded from the flex-shrink pool above, the
   arithmetic doesn't work at a 375px phone width even after trimming the
   button's own padding and dropping its arrow: logo (216px, protected) +
   toggle (42px) + the gaps between three items leaves roughly 50-60px for
   "Talk to us" to live in, nowhere near enough for the label to stay
   legible at any padding. Something has to leave the row rather than
   shrink further -- the logo's the one thing that can't, the toggle's
   already as small as a real tap target goes, so it's the button.
   Floating it (same element, same "Talk to us" text + arrow, still
   /contact/) rather than replacing it with an icon keeps the CTA's actual
   words in front of people instead of trading them for a glyph they have
   to interpret -- a phone icon in particular would misname the
   destination, since /contact/ is a page, not a tel: link.

   .cta-floating is added by initFloatingCta() in main.js at the same time
   it moves the button to a direct child of <body> -- required (not just
   tidiness) because .container carries backdrop-filter for its own glass-
   pill blur, and per spec that makes .container the containing block for
   any position: fixed descendant, which resolved this button's "bottom:
   20px" against .container's own ~70px-tall box near the top of the page
   instead of the viewport. Styled by this class rather than by
   ".header-actions .btn-primary" for the same reason -- that descendant
   selector stops matching the moment the button is no longer inside
   .header-actions. */
.cta-floating {
	position: fixed; top: auto; left: auto; z-index: 150; bottom: 1.25rem; right: 1.25rem; margin: 0;
	opacity: 0; visibility: hidden; pointer-events: none;
	transition: opacity .25s ease, visibility .25s;
}
/* Held back until the page has scrolled about a screen's worth: most pages
   open with their own hero CTA already on screen (the homepage's "Schedule
   a Consultation" sits at almost exactly this button's own corner before
   any scroll), so showing a second "talk to us" pill on top of the first
   read as two competing buttons rather than one. Past the hero, nothing
   else on the page is offering that path, which is the actual gap a
   floating CTA is for. */
body.has-scrolled .cta-floating { opacity: 1; visibility: visible; pointer-events: auto; }
/* :has(), not a plain descendant/sibling selector -- .cta-floating sits as
   a direct child of <body> once relocated, a sibling of #masthead rather
   than a descendant of it, so a selector rooted the usual way
   (".site-header.is-open .cta-floating") could never match it. */
body:has(#masthead.is-open) .cta-floating { display: none; }
@media (max-width: 45em) { /* 720px */
	/* Every real .grid-3/.grid-4 on this site is text-heavy cards (heading
	   + paragraph, sometimes a stat callout or sub-link list too) — never
	   the short icon tiles that stayed-at-2-up used to assume. Same
	   unreadable-at-2-up problem .grid-2 already gets a fix for just
	   below, so it gets the same one-column floor. */
	.grid-3, .grid-4, .grid.industries-strip { grid-template-columns: 1fr; }
	/* .grid-2 had no collapse rule at all — fine for the short stat-pair
	   content it originally shipped with, but a text-heavy .card (Payments
	   prod-replica's "Why choose us", with a paragraph plus a tag list)
	   squeezes into an unreadable single-word-per-line column at phone
	   width without one. */
	.grid-2 { grid-template-columns: 1fr; }
	.hero-stats { grid-template-columns: repeat(2, 1fr); }
	.stat-row { grid-template-columns: repeat(2, 1fr); }
	.perk-grid { grid-template-columns: 1fr; }
	section.section { padding: 4rem 0; }
	/* Scoped to .hero-copy so this only ever reaches the top hero's own CTA
	   row -- .hero-cta is also reused, unscoped, for .cta-band's closing
	   pitch and a couple of ad-hoc mid-page prompts, neither of which asked
	   for this. Left-aligned, auto-width, stacked one under the other (the
	   default with no rule here) read as an afterthought at this width, and
	   shrinking two buttons to fit one row isn't safe either -- CTA labels
	   run from "Talk to Us" to "See how we build agents like this" across
	   the site's ~50 hero instances, so a width narrow enough for the short
	   ones would just wrap or clip the long ones. Full width reads
	   decisively at any label length and gives a properly-sized tap target;
	   applied whether the hero has one CTA or two, so the pattern is the
	   same page to page rather than conditional on a count a visitor has no
	   way to anticipate. white-space/text-align undo .btn's own nowrap/
	   center-via-flex, which assumed a hug-content pill, not a full-width
	   one -- without them the longest labels would overflow past the
	   button's own edges instead of wrapping. */
	.hero-copy .hero-cta { flex-direction: column; }
	.hero-copy .hero-cta .btn { width: 100%; white-space: normal; text-align: center; }
}
@media (max-width: 32.5em) { /* 520px */
	.footer-top { grid-template-columns: 1fr; }
	.footer-bottom { flex-direction: column; align-items: flex-start; }
}

/* Mobile nav */
.site-header.is-open .primary-nav {
	display: block; position: absolute; top: 100%; left: 0; right: 0;
	background: var(--paper-hi); border-bottom: 1px solid var(--hair-l); padding: 1.25rem 0;
	/* .site-header is position: fixed, so this panel's containing block is
	   fixed too -- the page scrolls underneath it, but the panel itself
	   never moves and was never given a height limit, so on a short screen
	   with enough sub-items open (Services, previously always-open below)
	   its bottom half rendered off the bottom of the viewport with no way
	   to reach it: nothing scrolls the panel itself, and scrolling the page
	   only moves what's already fixed in place. --header-h is written by
	   the .menu-toggle click handler in main.js (header.offsetHeight varies
	   with .is-day's padding, so this reads the real value instead of
	   guessing one); the 90px fallback is only for the no-JS edge case. */
	max-height: calc(100dvh - var(--header-h, 5.625rem));
	overflow-y: auto;
	-webkit-overflow-scrolling: touch;
}
/* align-items: stretch (the flex default -- set explicitly since this used
   to override it to flex-start) so each top-level <li> takes the full row
   width on the cross axis instead of shrinking to its own content. Without
   it, .menu-item-has-children below has nothing to grow into: its <a>'s
   flex: 1 auto only has slack to fill if the <li> containing it is already
   full width. This was the actual reason the toggle arrow (previously
   a::after) landed hard against short labels but at the true right edge
   for whichever item happened to be open -- an open item's <li> also
   contains its now-visible .sub-menu, which IS full width, so only open
   items were ever getting the full-width box their arrow was measured
   against. */
.site-header.is-open .primary-nav ul { flex-direction: column; gap: 0.25rem; align-items: stretch; padding: 0 1.5rem; }
/* Each item is its own row: the link (grows to fill) beside the toggle
   button (fixed size, see .submenu-toggle below), wrapping the sub-menu
   onto a second line via its own flex-basis: 100% rather than a nested
   wrapper element. */
.site-header.is-open .primary-nav .menu-item-has-children { display: flex; flex-wrap: wrap; align-items: center; }
.site-header.is-open .primary-nav a { display: flex; align-items: center; padding: 0.75rem 0; color: var(--ink); }
.site-header.is-open .primary-nav .menu-item-has-children > a { flex: 1 1 auto; min-width: 0; }
/* The old a::after chevron was built for the desktop bar's inline-flex
   layout (see its own rule above) and never quite recovered under the
   mobile panel's block flow -- landing as a stray vertical sliver next to
   short labels, at the true row edge only for whichever item was open (the
   align-items fix above). .submenu-toggle replaces it here with a real,
   independently tappable button instead of trying to fix pseudo-content
   glued to the link -- see initDropdownNav() in main.js for why the two
   controls (label always navigates, button always opens/closes) are worth
   having separately rather than one link doing both depending on state. */
.site-header.is-open .primary-nav .menu-item-has-children > a::after { display: none; }
/* Every item's markup carries a real .submenu-toggle button (baked into
   each page, not built by JS -- see initDropdownNav() in main.js, which
   only wires up its click handler). Present regardless of viewport --
   simpler than only rendering it for touch, and this site's own resize
   handling elsewhere shows that "only wire it up for the mode active at
   load" doesn't survive a later resize across the hover/no-hover line
   anyway. Hidden by default (desktop's hover-opened dropdown doesn't need
   it) and shown only inside the open mobile panel, below. */
.submenu-toggle { display: none; }
.site-header.is-open .primary-nav .submenu-toggle {
	display: flex; align-items: center; justify-content: center; flex: none;
	width: 2.75rem; height: 2.75rem; margin: -0.75rem -0.625rem -0.75rem 0;
	background: none; border: none; color: var(--ink); cursor: pointer;
}
.site-header.is-open .primary-nav .submenu-toggle svg { transition: transform .2s ease; }
.site-header.is-open .primary-nav .menu-item-has-children.is-open > .submenu-toggle svg { transform: rotate(180deg); }
/* Collapsed by default -- previously every sub-menu was forced open
   (opacity/visibility overridden unconditionally below) the moment the
   panel itself opened, which is what made Services alone run the whole
   menu to several screens tall. initDropdownNav() in main.js already
   toggles .is-open per item on tap for any touch device (closing whichever
   other one was open first) and already worked correctly on the desktop
   nav bar -- this panel was just overriding its result back to "always
   shown" instead of reading it. flex-basis: 100% is what forces it onto
   its own row under the link + toggle row above, once visible. */
.site-header.is-open .primary-nav .sub-menu {
	position: static; margin: 0 0 0.25rem 1rem; padding: 0; border: none; box-shadow: none; background: transparent;
	display: none; flex-basis: 100%;
}
.site-header.is-open .primary-nav .menu-item-has-children.is-open > .sub-menu { display: block; }
.site-header.is-open .primary-nav .sub-menu::before { display: none; }
.site-header.is-open .primary-nav .sub-menu a { padding: 0.5rem 0; font-size: 0.875rem; color: var(--ink-70); }

/* ==========================================================================
   Reduced motion — collapse the stage and the stack to plain, static flow
   ========================================================================== */
@media (prefers-reduced-motion: reduce) {
	html { scroll-behavior: auto; }
	.hero-stage { height: auto; }
	.hero-stage__sticky { position: static; height: auto; }
	.hero { clip-path: none !important; border-radius: 0 !important; box-shadow: none !important; height: auto; min-height: 100vh; padding: 5.625rem 0 4.375rem; }
	.hero .container { transform: none !important; }
	.hero-headline .ln > span { transform: none; animation: none; }
	.stack > .stack-item { position: static; transform: none !important; filter: none !important; margin-bottom: 1.5rem; }
}

/* ==========================================================================
   Case studies — the evidence theme

   Extends the rule the stacked homepage sets, that dark carries proof: the
   article reads on paper, and every ink block on the page is a measured
   fact. Prose makes the claims, ink blocks carry the evidence, so a reader
   who skims only the dark bands still leaves with every number.

   The display face is the site's own Archivo, but set on a width axis the
   rest of the site never uses (wdth 84 against the homepage's 106-110).
   Same voice, different register — and narrow is what lets a 12-word case
   study title run large without wrapping to five lines.

   Every class here starts cs-. The Careers culture-sets carousel (near the
   foot of this file) used the same prefix, and a name defined in both applies
   to both: .cs-grid and .cs-title once were, which turned the index into two
   uneven columns under a 24px headline and shrank every case-study headline.
   The carousel's classes all start cset- now; keep the two prefixes apart.
   tools/check-css-collisions.py fails if a class is defined in two sections.
   ========================================================================== */
.case-study {
	--cs-ink: #0B1A1F;          /* between --hero-ink and --ink, teal-leaning */
	/* The baseline mark is quiet, but in a "before" row it IS the reading —
	   eight marks mean eight weeks — so it has to clear 3:1 as a graphic
	   rather than fade to texture. .38 puts it at 3.2:1 on the ink. */
	--cs-mark-off: rgba(234,243,241,.38);
	--cs-on-ink: #EAF3F1;
	/* The site's --ink-45 lands at 3.0:1 on this lighter paper, which is
	   under the floor for the mono labels and captions that use it here. */
	--cs-quiet: rgba(11,26,31,.64);
	background: #F2F5F4;        /* a shade lighter than --paper: a document, not a section */
}
/* Scoped to main, not the bare body class — the header and footer are
   body.case-study's other children too, and their .container needs to
   stay at the site's standard 1280px like every other page. */
.case-study main .container { max-width: 67.5rem; }

/* --- hero ---------------------------------------------------------------- */
.cs-hero { padding: 1.875rem 0 0.5rem; }
.cs-back {
	display: inline-block; font-family: var(--font-mono); font-size: var(--text-min);
	letter-spacing: .13em; text-transform: uppercase; color: var(--cs-quiet); margin-bottom: 1.875rem;
}
.cs-back:hover { color: var(--teal-deep); }
.cs-docket {
	display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem 0.875rem; margin-bottom: 1.25rem;
	font-family: var(--font-mono); font-size: var(--text-min); letter-spacing: .14em;
	text-transform: uppercase; color: var(--cs-quiet);
}
.cs-docket b { font-weight: 500; color: var(--teal-deep); }
.cs-docket i { font-style: normal; opacity: .4; }
.cs-title {
	font-family: var(--font-display);
	font-variation-settings: 'wdth' 84, 'wght' 760;
	font-size: clamp(2.15rem, 5.4vw, 4.1rem);
	line-height: 1.03; letter-spacing: -.021em; max-width: 22ch; color: var(--ink);
}
.cs-lede {
	margin-top: 1.375rem; max-width: 56ch; font-size: 1.16rem; line-height: 1.56; color: var(--ink-70);
}

/* --- the evidence block: the count strips --------------------------------
   Discrete marks with gaps, not a segmented bar — the whole point is that
   the reader can count them. Teal is always the state we delivered and the
   muted mark is always the baseline, so on a "less is better" metric the
   short coloured row wins and on a "more is better" one the long row does. */
.cs-evidence {
	margin: 2.75rem 0 0; padding: 2rem 2.125rem; border-radius: var(--radius-lg);
	background: var(--cs-ink); color: var(--cs-on-ink);
}
.cs-metric + .cs-metric { margin-top: 1.5rem; padding-top: 1.5rem; border-top: 1px solid rgba(234,243,241,.13); }
.cs-metric-head {
	display: flex; justify-content: space-between; align-items: baseline; gap: 1rem;
	font-family: var(--font-mono); font-size: var(--text-min); letter-spacing: .14em;
	text-transform: uppercase; color: rgba(234,243,241,.55);
}
.cs-metric-figure {
	font-family: var(--font-display); font-variation-settings: 'wdth' 90, 'wght' 720;
	font-size: 1.45rem; letter-spacing: -.01em; text-transform: none;
	color: var(--cs-on-ink); font-variant-numeric: tabular-nums; flex: none;
}
.cs-row { display: grid; grid-template-columns: 3.25rem 1fr auto; align-items: center; gap: 0.5rem 0.875rem; margin-top: 0.75rem; }
.cs-row-label {
	font-family: var(--font-mono); font-size: var(--text-min); letter-spacing: .1em;
	text-transform: uppercase; color: rgba(234,243,241,.55);
}
.cs-units { display: flex; flex-wrap: wrap; gap: 0.3125rem; }
.cs-unit { width: 0.8125rem; height: 0.8125rem; border-radius: 0.1875rem; background: var(--cs-mark-off); }
.cs-unit--on { background: var(--teal); }
.cs-row-figure {
	font-family: var(--font-mono); font-size: 0.75rem; color: rgba(234,243,241,.72);
	font-variant-numeric: tabular-nums; white-space: nowrap;
}
/* A metric with no proportion behind it ($4B a day, sub-50ms) gets the
   figure alone. Drawing twenty marks for it would invent a ratio. */
.cs-metric--figure .cs-metric-head { display: block; }
.cs-metric--figure .cs-metric-figure {
	/* clamp() replaces what used to be a hard jump to 1.75rem at 720px
	   (same two endpoints, now a smooth transition between them instead of
	   a snap) -- see the 720px block below, where that rule used to live. */
	display: block; margin-top: 0.375rem; font-size: clamp(1.75rem, 4vw, 2.1rem); font-variation-settings: 'wdth' 86, 'wght' 740;
}

/* --- body ---------------------------------------------------------------- */
.cs-body { padding: 4rem 0 0.5rem; }
.cs-block + .cs-block { margin-top: 3.5rem; }
.cs-h2 {
	font-family: var(--font-display); font-variation-settings: 'wdth' 92, 'wght' 680;
	font-size: 1.42rem; letter-spacing: -.012em; color: var(--ink); margin-bottom: 1.125rem;
	padding-bottom: 0.875rem; border-bottom: 1px solid rgba(11,26,31,.13);
}
.cs-prose { max-width: 68ch; font-size: 1.06rem; line-height: 1.72; color: var(--ink-70); }
.cs-prose p { margin-bottom: 1.125rem; }
.cs-prose p:last-child { margin-bottom: 0; }
.cs-prose strong { color: var(--ink); font-weight: 600; }

/* Challenges are a set of separate faults, not a ranked order, so they get
   dashes. The phases below are numbered because they genuinely ran in that
   sequence and the order is information the reader needs. */
.cs-faults { list-style: none; margin: 0; max-width: 68ch; }
.cs-faults li {
	position: relative; padding-left: 1.625rem; margin-bottom: 0.875rem;
	font-size: 1.04rem; line-height: 1.62; color: var(--ink-70);
}
.cs-faults li::before {
	content: ''; position: absolute; left: 0; top: .68em; width: 0.8125rem; height: 1px; background: var(--sun);
}

/* --- the phase rail ------------------------------------------------------ */
.cs-phases { list-style: none; margin: 0; position: relative; }
.cs-phases::before {
	content: ''; position: absolute; left: 0.9375rem; top: 0.75rem; bottom: 0.75rem; width: 1px;
	background: rgba(11,26,31,.14);
}
.cs-phase { position: relative; padding-left: 3.25rem; padding-bottom: 1.875rem; }
.cs-phase:last-child { padding-bottom: 0; }
.cs-phase-num {
	position: absolute; left: 0; top: -2px; width: 1.9375rem; height: 1.9375rem; border-radius: 50%;
	display: flex; align-items: center; justify-content: center;
	background: var(--cs-ink); color: var(--cs-on-ink);
	font-family: var(--font-mono); font-size: var(--text-min); font-variant-numeric: tabular-nums;
}
.cs-phase h3 {
	font-family: var(--font-display); font-variation-settings: 'wdth' 96, 'wght' 660;
	font-size: 1.06rem; color: var(--ink); margin-bottom: 0.5rem;
}
.cs-phase ul { list-style: none; margin: 0; }
.cs-phase li {
	position: relative; padding-left: 1.125rem; margin-bottom: 0.4375rem;
	font-size: 1rem; line-height: 1.6; color: var(--ink-70);
}
.cs-phase li::before {
	content: ''; position: absolute; left: 0; top: .66em; width: 0.3125rem; height: 0.3125rem;
	border-radius: 50%; background: var(--teal);
}
/* A phase told in full sentences rather than bullets (the case studies
   rewritten in the production voice narrate each step as a short
   paragraph). Same size and colour as the list items it replaces. */
.cs-phase p { max-width: 68ch; font-size: 1rem; line-height: 1.6; color: var(--ink-70); }
/* "Here's how we implemented…" introduces the steps as a full sentence, so
   it needs air before the first step's circle; without it the 01 marker
   sits flush under the line. Only pages that lead into the steps this way
   match, so the rail elsewhere keeps its spacing. */
.cs-prose + .cs-phases { margin-top: 1.5rem; }

/* --- outcomes ------------------------------------------------------------ */
.cs-outcomes { list-style: none; margin: 0; max-width: 68ch; }
.cs-outcomes li {
	display: flex; gap: 0.75rem; align-items: flex-start; margin-bottom: 0.8125rem;
	font-size: 1.04rem; line-height: 1.62; color: var(--ink-70);
}
.cs-outcomes svg { flex: none; width: 1.0625rem; height: 1.0625rem; margin-top: 0.25rem; color: var(--teal-deep); }

/* --- stack --------------------------------------------------------------- */
.cs-stack { display: flex; flex-wrap: wrap; gap: 0.5625rem; }
.cs-stack span {
	padding: 0.4375rem 0.9375rem; border-radius: 999px; background: #fff;
	border: 1px solid rgba(11,26,31,.13); font-family: var(--font-mono);
	font-size: .8rem; color: var(--ink);
}

/* --- figures -------------------------------------------------------------
   Deliberately legible as placeholders: an in-brand line drawing of the
   right subject, a corner badge, and a title tooltip spelling out what the
   real photograph should show. Reads as designed rather than missing, and
   tells whoever sources the image exactly what to look for. */
.cs-figure { margin: 1.625rem 0 0; }
.cs-figure-frame {
	position: relative; border-radius: var(--radius); overflow: hidden;
	background: var(--cs-ink); border: 1px solid rgba(11,26,31,.13); cursor: help;
}
/* Finished figure (real photo or a real diagram, not a photo-brief wireframe)
   — no tooltip to invite a hover for, so the help cursor doesn't apply. */
.cs-figure-frame--final { cursor: default; }
.cs-figure-frame svg { display: block; width: 100%; height: auto; }
/* The claims case study's two "final" figures are text-heavy diagrams, not
   photos — the rule above is right for a photo (scaling down loses nothing)
   but would take a 9.5px viewBox label down toward 3px on a phone, where
   .cs-figure-frame's plain block-flow container (unlike .capability-media's
   grid, which incidentally resists shrinking) offers no floor at all. Scoped
   to just these two figures via their own wrapper, so the site's normal
   photo figures keep the width:100% behavior above untouched. */
.cs-figure-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }
.cs-figure-scroll svg { width: 100%; min-width: 60rem; height: auto; }
/* Real photo, in place of the wireframe: same 12:5 stage the wireframe drew
   at (viewBox 1200x500), so a figure that's already real and one still
   waiting on a photo sit at the same height rather than jumping around. */
.cs-figure-frame img { display: block; width: 100%; height: auto; aspect-ratio: 12 / 5; object-fit: cover; object-position: 50% 42%; }
.cs-figure figcaption {
	margin-top: 0.6875rem; font-family: var(--font-mono); font-size: var(--text-min);
	line-height: 1.5; color: var(--cs-quiet); max-width: 62ch;
}

/* --- index grid ----------------------------------------------------------
   The old cards carried generated gradient covers in colours from no part
   of this palette. A card now previews the case study in the same language
   the case study itself uses: its headline outcome, counted. That makes the
   index scannable by result, which is what anyone lands on it looking for. */
.cs-index { padding: 0.25rem 0 4.75rem; }
.cs-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.375rem; }
.cs-card {
	display: flex; flex-direction: column; padding: 1.625rem 1.625rem 1.375rem; color: inherit; overflow: hidden;
	background: #fff; border: 1px solid rgba(11,26,31,.13); border-radius: var(--radius-lg);
	transition: border-color .2s ease, transform .35s var(--ease), box-shadow .35s;
}
.cs-card:hover { border-color: var(--teal); transform: translateY(-0.3125rem); box-shadow: var(--shadow-soft); }
/* Bleeds to the card edge through the card's own padding; the card clips it
   to the corner radius so the media needs no radius of its own. */
.cs-card-media { margin: -1.625rem -1.625rem 1.25rem; aspect-ratio: 16 / 9; overflow: hidden; }
.cs-cover { display: block; width: 100%; height: 100%; }
.cs-card:hover .cs-cover { transform: scale(1.03); }
.cs-cover { transition: transform .5s var(--ease); }
@media (prefers-reduced-motion: reduce) {
	.cs-cover { transition: none; }
	.cs-card:hover .cs-cover { transform: none; }
}
.cs-card-industry {
	font-family: var(--font-mono); font-size: var(--text-min); letter-spacing: .14em;
	text-transform: uppercase; color: var(--cs-quiet);
}
.cs-card h3 {
	margin: 0.75rem 0 0.625rem; font-family: var(--font-display);
	font-variation-settings: 'wdth' 86, 'wght' 720; font-size: 1.3rem;
	line-height: 1.15; letter-spacing: -.018em; color: var(--ink);
}
.cs-card p { font-size: .95rem; line-height: 1.55; color: var(--ink-70); margin-bottom: 1.125rem; }
.cs-card-strip { margin-top: auto; padding: 0.9375rem 1rem; border-radius: 0.75rem; background: var(--cs-ink); }
.cs-card-strip .cs-metric-head { font-size: var(--text-min); letter-spacing: .12em; }
.cs-card-strip .cs-metric-figure { font-size: 1rem; }
.cs-card-strip .cs-units { gap: 0.25rem; margin-top: 0.5625rem; }
.cs-card-strip .cs-unit { width: 0.625rem; height: 0.625rem; border-radius: 2px; }
.cs-card-strip .cs-units + .cs-units { margin-top: 0.3125rem; }
.cs-card .card-link { margin-top: 1rem; color: var(--teal-deep); }
@media (max-width: 61.25em) { /* 980px */ .cs-grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 43.75em) { /* 700px */ .cs-grid { grid-template-columns: 1fr; } }

/* --- next case study ----------------------------------------------------- */
.cs-next { padding: 3.5rem 0 4.5rem; }
.cs-next-card {
	display: block; padding: 1.75rem 2rem; border-radius: var(--radius-lg);
	background: #fff; border: 1px solid rgba(11,26,31,.13); color: inherit;
	transition: border-color .2s ease, transform .35s var(--ease);
}
.cs-next-card:hover { border-color: var(--teal); transform: translateY(-0.25rem); }
.cs-next-label {
	font-family: var(--font-mono); font-size: var(--text-min); letter-spacing: .14em;
	text-transform: uppercase; color: var(--cs-quiet);
}
.cs-next-card h3 {
	margin-top: 0.625rem; font-family: var(--font-display);
	font-variation-settings: 'wdth' 90, 'wght' 700; font-size: 1.3rem;
	letter-spacing: -.014em; color: var(--ink); max-width: 26ch;
}

@media (max-width: 45em) { /* 720px */
	.cs-evidence { padding: 1.5rem 1.25rem; }
	.cs-row { grid-template-columns: 1fr; gap: 0.375rem; }
	.cs-row-figure { justify-self: start; }
	.cs-body { padding: 3rem 0 0.5rem; }
	.cs-block + .cs-block { margin-top: 2.75rem; }
	.cs-phase { padding-left: 2.75rem; }
	.cs-next-card { padding: 1.375rem 1.25rem; }
}
@media (prefers-reduced-motion: reduce) {
	.cs-next-card { transition: none; }
	.cs-next-card:hover { transform: none; }
}

/* ==========================================================================
   Blog — the field-notes theme

   A case study proves a result; a blog post explains a mechanism. So it
   does not repeat the case study's dark evidence-ledger move — it reads as
   documentation: white page, hairline rules, a wide reading column. The one
   device it borrows from the rest of the site is the family trait of
   "show the read as a trace" (the hero's run panel, the lead card's
   waterfall, the case study's count strip) — here that trait becomes the
   run log: a sticky section rail that ticks off as you scroll, standing in
   for a table of contents on posts that run past 1500 words and 20
   headings. Short posts don't earn one and don't get one.

   Figures use the same dark-ink frame + tooltip convention as case-study
   figures (deliberately legible as an illustration, not a stock photo),
   drawn from a shared motif vocabulary — posts on the same topic share a
   motif on purpose, the way real documentation does.
   ========================================================================== */
.blog-post {
	background: #fff;
	/* .5/.42/.46-alpha ink read as ~3.0-3.5:1 on this page's pure-white
	   background — under the 4.5:1 floor for body-size text, the same
	   mistake caught once already in the case-study theme (--cs-quiet) and
	   repeated here with a fresh, unchecked value. One token, checked once:
	   rgba(16,22,25,.64) measures ~5.4:1 on white. */
	--blog-quiet: rgba(16,22,25,.64);
}
/* Scoped to main, not the bare body class — same header/footer leak as
   .case-study .container, fixed the same way. */
.blog-post main .container { max-width: 72.5rem; }

/* --- hero ------------------------------------------------------------- */
.blog-hero { padding: 2.125rem 0 0; }
.blog-back {
	display: inline-block; font-family: var(--font-mono); font-size: var(--text-min);
	letter-spacing: .13em; text-transform: uppercase; color: var(--blog-quiet); margin-bottom: 1.75rem;
}
.blog-back:hover { color: var(--teal-deep); }
.blog-meta {
	display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem 0.875rem; margin-bottom: 1.125rem;
	font-family: var(--font-mono); font-size: var(--text-min); letter-spacing: .13em;
	text-transform: uppercase; color: var(--blog-quiet);
}
.blog-meta b { font-weight: 500; color: var(--teal-deep); }
.blog-meta i { font-style: normal; opacity: .45; }
.blog-title {
	font-family: var(--font-display); font-variation-settings: 'wdth' 100, 'wght' 720;
	font-size: clamp(2rem, 4.6vw, 3.15rem); line-height: 1.08; letter-spacing: -.017em;
	max-width: 20ch; color: var(--ink);
}
.blog-dek { margin-top: 1.125rem; max-width: 62ch; font-size: 1.14rem; line-height: 1.55; color: var(--ink-70); }
.blog-byline {
	margin-top: 1.375rem; padding-top: 1.125rem; border-top: 1px solid var(--hair-l);
	font-size: .88rem; color: var(--blog-quiet);
}
.blog-byline b { color: var(--ink); font-weight: 600; }

/* --- body split: rail + column ----------------------------------------- */
/* minmax(0, 1fr), not a bare 1fr: `1fr` means minmax(auto, 1fr), and that
   auto floor stops the reading column shrinking below its widest child — so
   a wide figure or code block pushes the column past its track and out under
   body's overflow-x:clip instead of scrolling inside it. */
.blog-layout { display: grid; grid-template-columns: 12.5rem minmax(0, 1fr); gap: 3.5rem; align-items: start; padding: 2.875rem 0 0; }
.blog-layout .blog-column { min-width: 0; }
.blog-layout:not(.has-rail) { grid-template-columns: 1fr; }
/* width: 100% alongside max-width, not just min-width: 0 on the item --
   min-width: 0 stops the grid track itself from being forced wide by this
   column's content, but a bare max-width + margin: auto still lets the
   column's own box size to its content's preferred (max-content) width
   before capping/centering it. Harmless when that content is short lines
   of prose, which just wrap -- but a post with a wide code block or table
   can still push the column past its 327px-ish phone-width track and off
   the right edge (confirmed on blog-advanced-auditing-hibernate-envers's
   long unwrapped SQL/annotation lines; site-wide overflow-x: clip hides
   the resulting horizontal scrollbar, so it silently clips instead of
   visibly breaking). width: 100% forces it to actually fill the track;
   max-width: 720px still wins and caps it once the track is wider than
   that, so nothing changes at tablet width and up. */
.blog-layout:not(.has-rail) .blog-column { width: 100%; max-width: 45rem; margin: 0 auto; }

/* --- the run log --------------------------------------------------------
   Section titles pulled verbatim from the post's own headings, so the rail
   can never claim a structure the article doesn't have. A dot fills in as
   its section is read; the current section gets the teal line. Ticked by
   initBlogLog() in main.js the same way the homepage run waterfall and
   card-stack are driven — an IntersectionObserver flipping classes, no
   scroll math. */
/* A few of these posts run past 15 sections (scala's runs 19) — tall enough
   that the rail itself exceeds a normal viewport. Left plain sticky, the
   entries past the fold would never scroll into view: the rail pins at
   top:104px and everything below the viewport's bottom edge simply stays
   off-screen for the rest of the read. It scrolls internally instead. */
.blog-rail { position: sticky; top: 6.5rem; max-height: calc(100vh - 8.125rem); overflow-y: auto; scrollbar-width: thin; }
.blog-rail-label {
	font-family: var(--font-mono); font-size: var(--text-min); letter-spacing: .14em;
	text-transform: uppercase; color: var(--blog-quiet); margin-bottom: 0.875rem;
}
.blog-rail ol { list-style: none; margin: 0; padding: 0; position: relative; }
.blog-rail ol::before {
	content: ''; position: absolute; left: 0.3125rem; top: 0.375rem; bottom: 0.375rem; width: 1px; background: var(--hair-l);
}
.blog-rail li { position: relative; padding: 0 0 1rem 1.375rem; }
.blog-rail li:last-child { padding-bottom: 0; }
.blog-rail a {
	display: block; font-size: .84rem; line-height: 1.4; color: var(--blog-quiet);
	transition: color .2s ease;
}
.blog-rail li::before {
	content: ''; position: absolute; left: 0; top: 0.25rem; width: 0.6875rem; height: 0.6875rem; border-radius: 50%;
	background: #fff; border: 1.5px solid rgba(16,22,25,.58); transition: background .2s ease, border-color .2s ease;
}
.blog-rail li.is-read::before { background: var(--teal); border-color: var(--teal); }
.blog-rail li.is-current a { color: var(--ink); font-weight: 600; }
.blog-rail li.is-current::before { border-color: var(--teal); box-shadow: 0 0 0 3px rgba(93,160,172,.18); }
.blog-rail a:hover { color: var(--teal-deep); }

/* ==========================================================================
   Service rail — a service-page-only replacement for .blog-rail, piloted
   on Cloud Engineering. Modelled on the quiet sidebar pattern seen on
   euphoricthought.com/cloud-consulting/: no card, no icons, plain text —
   the only signal is weight/colour on the active label and a single slim
   bar that slides to track it. Deliberately quieter than a blog widget so
   it reads as sales-page chrome, not editorial furniture. Ticked by
   initSvcRail() in main.js: an IntersectionObserver flips .is-current
   (same mechanism as initBlogLog) and the indicator's offsetTop/height are
   read off the current <li> and transitioned in place.
   ========================================================================== */
/* Bottom breathing room before the cta-band — .blog-layout itself only
   pads its top (blog posts space themselves off whatever follows some
   other way), and the last .svc-feature card has no margin of its own, so
   without this the card's background ran straight into the band with zero
   gap. 64px matches .svc-proof's own margin-bottom earlier on this same
   page rather than inventing a new spacing value. */
.blog-layout.has-svc-rail { grid-template-columns: 13.75rem minmax(0, 1fr); padding-bottom: 4rem; }
.svc-rail { position: sticky; top: 6.5rem; }
.svc-rail-eyebrow {
	font-size: var(--text-min); letter-spacing: .09em; text-transform: uppercase;
	color: var(--ink-70); opacity: .55; margin-bottom: 1.125rem;
}
.svc-rail ol { list-style: none; margin: 0; padding: 0; position: relative; }
.svc-rail-item { padding: 0 0 1.5rem 1.375rem; }
.svc-rail-item:last-child { padding-bottom: 0; }
.svc-rail-item a {
	display: block; font-size: .98rem; line-height: 1.4; font-weight: 500;
	color: var(--ink-70); transition: color .2s ease, font-weight .2s ease;
}
.svc-rail-item.is-current a { color: var(--ink); font-weight: 700; }
.svc-rail-item a:hover { color: var(--teal-deep); }
.svc-rail-indicator {
	position: absolute; left: 0; top: 0; width: 2px; border-radius: 2px;
	background: var(--teal); transition: transform .3s var(--ease), height .3s var(--ease);
}

/* ==========================================================================
   Service feature card — image-right/copy-left breakdown for each
   sub-service, piloted on Cloud Engineering in place of the plain
   .blog-prose block. One flat background (var(--muted), the same quiet
   panel tone already used for the newsletter field)
   reused across all three cards on purpose — a different colour per card
   would read as per-topic theming; one colour reads as a single family of
   cards. Needs the full column width the image deserves, so it opts the
   column out of the 720px reading measure .blog-column normally keeps for
   prose-only sections.
   ========================================================================== */
.has-svc-rail .blog-column { max-width: none; }
.svc-feature {
	display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); align-items: stretch;
	background: var(--muted); border-radius: 1.375rem; overflow: hidden;
}
.svc-feature-copy { padding: 2.75rem 2.875rem; display: flex; flex-direction: column; justify-content: center; }
.svc-feature-h2 { font-family: var(--font-display); font-size: 1.6rem; margin: 0 0 0.875rem; color: var(--ink); }
.svc-feature-lead { font-size: 1rem; line-height: 1.65; color: var(--ink-70); margin: 0 0 1.5rem; }
.svc-feature-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.8125rem; }
.svc-feature-list li { display: flex; align-items: flex-start; gap: 0.6875rem; font-size: .92rem; line-height: 1.5; color: var(--ink-70); }
.svc-feature-list svg { flex: none; width: 1.0625rem; height: 1.0625rem; margin-top: 2px; color: var(--teal-deep); }
/* These three are vendor/stock illustrations and one literal lifecycle
   diagram — not full-bleed photography — so object-fit: cover would crop
   into the diagram's own labels (the DevOps loop's tooling captions run
   right to its edges). contain on a plain white inset shows each graphic
   whole instead, with its own light background reading as intentional
   rather than as a leftover box. */
.svc-feature-media {
	position: relative; min-height: 22.5rem; background: var(--paper-hi);
	display: flex; align-items: center; justify-content: center; padding: 2rem;
}
.svc-feature-media img { display: block; max-width: 100%; max-height: 100%; width: auto; height: auto; object-fit: contain; }
/* Fill variant, piloted on Business Intelligence -- its three card images
   (a landscape isometric illustration, a tall portrait infographic, a tall
   portrait photo) don't share Cloud Engineering's edge-to-edge-label
   problem that made contain the right call there, and against this card's
   roughly-landscape box, contain left visible dead space beside the two
   portrait images. cover crops instead of leaving that gap; object-position
   is set per image below where a naive centre crop would cut into
   meaningful content (a labelled top-to-bottom flow diagram) rather than
   just background. */
.svc-feature-media--fill { padding: 0; }
.svc-feature-media--fill img { width: 100%; height: 100%; max-width: none; max-height: none; object-fit: cover; }

/* Illustration variant — an alternative to the stock-image-in-a-white-inset
   media slot above. One tint across the whole card (no copy/media seam),
   and a white-balanced illustration that bleeds into that tint instead of
   sitting in a box: the QA family's .qa-topic-media fade, brought onto
   .svc-feature. Piloted on AI Native Product Engineering, whose four
   illustrations were all white-balanced to #EEF2F4; --feature-tint (and
   --feature-accent) are overridable so another family can adopt this with
   its own images, the same inline-override pattern .qa-topic already uses
   for --topic-tint.

   --illus-offset drops the picture's top edge below the card's own top
   padding, and defaults to 120px — level with the first line of the lead,
   not the h2, so the heading keeps its own line and the tint that would
   otherwise pool below the image moves up beside the title where the h2
   anchors it. That only pays for itself when there's enough copy for tint
   to pool in the first place: Cloud Engineering's descriptions run shorter
   than AI Native's, so it overrides --illus-offset to 44px inline (equal
   to .svc-feature-copy's own top padding) and the picture lines up with
   the title instead. */
.svc-feature--illus {
	--feature-tint: #EEF2F4;
	background: var(--feature-tint);
}
/* On .svc-feature-h2, not .svc-feature-copy -- .svc-feature-copy becomes
   display: contents on mobile (see that breakpoint below), which drops
   any ::before/::after of its own along with the box that would have
   carried it. h2 stays a real box at every width, so anchoring the
   accent bar there survives the mobile layout instead of vanishing. */
.svc-feature--illus .svc-feature-h2::before {
	content: ''; display: block; width: 2.125rem; height: 0.1875rem; border-radius: 2px;
	background: var(--feature-accent, var(--teal-deep)); margin-bottom: 1.125rem;
}
.svc-feature--illus .svc-feature-media {
	background: var(--feature-tint);
	min-height: 0; padding: 0;
	align-self: start; aspect-ratio: 1 / 1;
	margin-top: var(--illus-offset, 7.5rem);
	overflow: hidden;
}
.svc-feature--illus .svc-feature-media img {
	width: 100%; height: 100%; max-width: none; max-height: none;
	object-fit: cover; object-position: center;
}
/* the picture's inner and lower edges dissolve into the tint — no cut line
   between the grid cells, and no hard stop where the image ends and the
   card tint carries on below it */
.svc-feature--illus .svc-feature-media::after {
	content: ''; position: absolute; inset: 0; pointer-events: none;
	background:
		linear-gradient(to right,  var(--feature-tint) 0, transparent 15%),
		linear-gradient(to bottom, transparent 80%, var(--feature-tint) 100%),
		linear-gradient(to top,    transparent 92%, var(--feature-tint) 100%);
}

/* --- column -------------------------------------------------------------- */
.blog-column { max-width: 45rem; }

.blog-section { margin-top: 3.25rem; scroll-margin-top: 6.875rem; }
.blog-section:first-of-type { margin-top: 2.5rem; }
.blog-h2 {
	font-family: var(--font-display); font-variation-settings: 'wdth' 104, 'wght' 660;
	font-size: 1.46rem; letter-spacing: -.013em; color: var(--ink); margin-bottom: 1.125rem;
}
.blog-h3 {
	font-family: var(--font-sans); font-weight: 700; font-size: 1.05rem;
	color: var(--ink); margin: 1.875rem 0 0.75rem;
}
.blog-prose { font-size: 1.06rem; line-height: 1.75; color: var(--ink-70); }
.blog-prose p { margin-bottom: 1.125rem; }
.blog-prose p:last-child { margin-bottom: 0; }
.blog-prose strong { color: var(--ink); font-weight: 600; }
/* break-word: a bare URL used as link text (SchemaSpy's tool list) wraps
   inside a phone-width column instead of running past its edge */
.blog-prose a { color: var(--teal-deep); text-decoration: underline; text-underline-offset: 2px; overflow-wrap: break-word; }
/* Lists -- one set of marks on every post.  The old marks were a 1px dash
   and a 12px numeral pinned to the top of the line: easy to miss, and the
   numeral floated above the item it numbered.
   - A bullet is a 6px teal dot centred on the first line's x-height, which
     sits .925em down a Public Sans line at line-height 1.65.  One level down
     it opens into a ring and below that it's a short dash, so a nested point
     never reads as another top-level one.
   - A number sits in a round badge -- a light cousin of the case-study phase
     numbers -- centred on the item's first line, so it lines up with a bold
     item title (the "title, then paragraph" items) as well as plain text.  A
     numbered list inside an item counts in letters, so sub-points don't look
     like more steps.
   - An item that carries paragraphs, code or a nested list gets more air
     below it than a one-line item. */
.blog-prose ul, .blog-prose ol { margin: 0 0 1.125rem; padding: 0; }
.blog-prose li { position: relative; margin-bottom: 0.625rem; line-height: 1.65; list-style: none; }
.blog-prose ul > li { padding-left: 1.5rem; }
.blog-prose ul > li::before {
	content: ''; position: absolute; left: 0.25rem; top: calc(.925em - 0.1875rem);
	width: 0.375rem; height: 0.375rem; border-radius: 50%; background: var(--teal-deep);
}
.blog-prose li ul > li::before { background: none; border: 1.5px solid var(--teal-deep); }
.blog-prose li li ul > li::before {
	top: calc(.925em - .75px); width: 0.5rem; height: 1.5px; border: 0; border-radius: 1px; background: var(--teal-deep);
}
.blog-prose ol { counter-reset: blog-ol; }
.blog-prose ol > li { counter-increment: blog-ol; padding-left: 2.25rem; }
/* .74em is the badge numeral (~12.5px).  1.65em / .74 is the item's own
   line box in those units, so the top offset centres the 24px badge on it
   at any prose size (the callout's prose is a touch smaller). */
.blog-prose ol > li::before {
	content: counter(blog-ol); position: absolute; left: 0; top: calc((1.65em / .74 - 1.5rem) / 2);
	width: 1.5rem; height: 1.5rem; border: 1px solid rgba(47,126,142,.3); border-radius: 50%;
	background: var(--accent); color: var(--teal-ink); text-align: center;
	font: 500 .74em/1.375rem var(--font-mono); font-variant-numeric: tabular-nums;
}
.blog-callout .blog-prose > ol > li::before { background: var(--paper-hi); }
/* Letters for a numbered list inside an item.  The line box matches the
   item's, and .1em down puts DM Mono's baseline on Public Sans's. */
.blog-prose li ol > li { padding-left: 1.625rem; }
.blog-prose li ol > li::before {
	content: counter(blog-ol, lower-alpha) '.'; top: .1em; width: auto; height: auto;
	border: 0; border-radius: 0; background: none; color: var(--teal-deep); text-align: left;
	font: 500 .82em var(--font-mono); line-height: calc(1.65em / .82);
}
.blog-prose li > ul, .blog-prose li > ol { margin: 0.625rem 0 0; }
.blog-prose li > p { margin: 0.375rem 0 0; }
.blog-prose li > p:first-child { margin-top: 0; }
.blog-prose li > p + p { margin-top: 0.75rem; }
.blog-prose li > .blog-code-wrap { margin: 0.75rem 0 0; }
.blog-prose li:has(> p, > .blog-code-wrap, > ul, > ol) { margin-bottom: 1.25rem; }

/* --- tables --------------------------------------------------------------- */
.blog-table-wrap { overflow-x: auto; margin: 0.5rem 0 1.5rem; border: 1px solid var(--hair-l); border-radius: var(--radius); }
.blog-table-wrap table { width: 100%; border-collapse: collapse; font-size: .92rem; }
.blog-table-wrap th, .blog-table-wrap td { padding: 0.75rem 1rem; text-align: left; border-bottom: 1px solid var(--hair-l); }
.blog-table-wrap th {
	font-family: var(--font-mono); font-size: var(--text-min); letter-spacing: .08em; text-transform: uppercase;
	color: var(--blog-quiet); background: var(--paper); font-weight: 500;
}
.blog-table-wrap tr:last-child td { border-bottom: none; }

/* --- code blocks -----------------------------------------------------------
   Reuses --hero-ink, the same dark the hero's own code card and the case
   study figures sit on — one "ink" across the site, not a fourth invented
   dark. Token colours are the brand's own: teal for keywords, butter for
   literals, dim off-white for punctuation. */
.blog-code-wrap { margin: 0.5rem 0 1.5rem; border-radius: var(--radius); overflow: hidden; background: var(--hero-ink); }
.blog-code-head {
	display: flex; align-items: center; justify-content: space-between; padding: 0.625rem 1.125rem;
	border-bottom: 1px solid rgba(234,243,241,.1);
	font-family: var(--font-mono); font-size: var(--text-min); letter-spacing: .12em; text-transform: uppercase;
	color: rgba(234,243,241,.62);
}
.blog-code-wrap pre { margin: 0; padding: 1.125rem 1.25rem; overflow-x: auto; }
/* A code block sits nested inside .blog-prose (renderBody's output is
   injected straight into it), so this and .blog-prose code below are equal
   specificity (one class + one type each) — without the extra qualifier
   here, source order alone decides the winner and the inline-code style
   silently overrides this block's colour instead of losing to it. */
.blog-prose .blog-code-wrap code {
	display: block; font-family: var(--font-mono); font-size: .85rem; line-height: 1.65;
	color: rgba(234,243,241,.92); white-space: pre; background: none; padding: 0; border-radius: 0;
}
.blog-code-wrap .kw1, .blog-code-wrap .kw2, .blog-code-wrap .kw3, .blog-code-wrap .kw4,
.blog-code-wrap .re1, .blog-code-wrap .re2, .blog-code-wrap .sc3 { color: var(--teal); }
.blog-code-wrap .co1 { color: rgba(234,243,241,.46); font-style: italic; }
.blog-code-wrap .co2 { color: rgba(234,243,241,.66); }
.blog-code-wrap .st0, .blog-code-wrap .nu0 { color: var(--butter); }
.blog-code-wrap .me1 { color: rgba(234,243,241,.85); }
.blog-code-wrap .sy0, .blog-code-wrap .br0 { color: rgba(234,243,241,.4); }
.blog-code-wrap .es0 { color: var(--sun); }
/* Inline `code` inside a paragraph — not a block, no dark card. */
.blog-prose code {
	font-family: var(--font-mono); font-size: .88em; background: var(--accent);
	color: var(--teal-deep); padding: .1em .4em; border-radius: 0.3125rem;
}

/* --- figures ---------------------------------------------------------------
   Same convention as the case-study figures: an in-brand line drawing
   and a tooltip that reads as a brief for whoever sources the real
   photograph or diagram later. */
.blog-figure { margin: 1.875rem 0; }
.blog-figure-frame {
	position: relative; border-radius: var(--radius); overflow: hidden;
	background: var(--hero-ink); border: 1px solid var(--hair-l); cursor: help;
}
.blog-figure-frame svg { display: block; width: 100%; height: auto; }
.blog-figure figcaption {
	margin-top: 0.6875rem; font-family: var(--font-mono); font-size: var(--text-min);
	line-height: 1.5; color: rgba(16,22,25,.64); max-width: 62ch;
}
.blog-figure--hero { margin: 1.875rem 0 0; }
/* A resolved figure: the real photo/diagram is in, so the wireframe's help
   cursor and dark placeholder ground no longer apply — just frame the image. */
.blog-figure-frame--final { cursor: default; background: none; border-color: var(--hair-l); }
.blog-figure-frame--final img { display: block; width: 100%; height: auto; }
/* A screenshot or diagram narrower than the column (the certificate dialog,
   the FIX logon sequence) is shown at its own pixel size, centred on a quiet
   mat, instead of being stretched -- and blurred -- to the column's width. */
.blog-figure-frame--natural { padding: clamp(0.875rem, 4vw, 2rem); background: var(--paper); }
.blog-figure-frame--natural img { width: auto; max-width: 100%; margin: 0 auto; box-shadow: 0 0 0 1px var(--hair-l); }
/* A run of many stock photos from different sources (e.g. Top Technology
   Trends' ten topic photos) reads as ten unrelated downloads unless
   something ties them together. A shared crop ratio plus one consistent
   dark-teal wash does that without touching the photos themselves —
   cheaper and more honest than re-grading each one individually. */
.blog-figure-frame--topic-photo { aspect-ratio: 16 / 9; }
.blog-figure-frame--topic-photo img { width: 100%; height: 100%; object-fit: cover; }
.blog-figure-frame--topic-photo::after {
	content: ""; position: absolute; inset: 0; pointer-events: none;
	background: linear-gradient(160deg, rgba(12,36,44,.24) 0%, rgba(12,36,44,.62) 100%);
}
/* A finished, on-brand vector illustration — not a placeholder brief (no
   brief to reveal on hover) and not a photo (keeps the dark figure ground
   the artwork is drawn against, unlike --final which clears it for a photo's
   own colour). */
.blog-figure-frame--illustration { cursor: default; }
.blog-figure-frame--illustration svg text { user-select: none; }

/* --- closing callout -------------------------------------------------------
   The last top-level section of every one of these posts is structurally a
   wrap-up ("The Bottom Line", "Conclusion", "Wrapping Up" ...) — that isn't
   a per-post judgment call, it's true of all 22 source posts, so it always
   gets the same distinct treatment: the one place teal fills a surface. */
/* margin-bottom mirrors margin-top: .blog-callout is always the last thing
   in .blog-column. On a blog post that's fine — .blog-next follows right
   after as its own section with 52px of top padding — but on a legal page
   (accessibility, privacy, terms) nothing follows it at all, so without its
   own bottom margin the callout sits flush against the footer. */
.blog-callout {
	margin-top: 3.25rem; margin-bottom: 3.25rem; padding: 1.875rem 2rem; border-radius: var(--radius-lg);
	background: var(--accent); border: 1px solid rgba(93,160,172,.22);
}
.blog-callout .blog-h2 { font-size: 1.2rem; margin-bottom: 0.75rem; }
.blog-callout .blog-prose { font-size: 1.02rem; color: var(--ink-70); }
.blog-callout .blog-prose p:last-child { margin-bottom: 0; }

/* --- next article --------------------------------------------------------- */
.blog-next { padding: 3.25rem 0 4.5rem; }
.blog-next-card {
	display: block; padding: 1.625rem 1.875rem; border-radius: var(--radius-lg);
	background: var(--paper); border: 1px solid var(--hair-l); color: inherit;
	transition: border-color .2s ease, transform .35s var(--ease);
}
.blog-next-card:hover { border-color: var(--teal); transform: translateY(-0.25rem); }
.blog-next-label {
	font-family: var(--font-mono); font-size: var(--text-min); letter-spacing: .14em;
	text-transform: uppercase; color: var(--blog-quiet);
}
.blog-next-card h3 {
	margin-top: 0.625rem; font-family: var(--font-display);
	font-variation-settings: 'wdth' 96, 'wght' 700; font-size: 1.24rem;
	letter-spacing: -.012em; color: var(--ink); max-width: 30ch;
}

/* --- index grid ------------------------------------------------------------
   Each card previews the post with the same motif drawing used as that
   post's own hero figure — open the card and you land on the same
   illustration, larger, rather than a different image entirely. The
   paragraph gives up two lines to make room for it; a card that has to
   choose between an image and a long excerpt reads better with the image. */
.blog-index-hero { padding: 1.875rem 0 0.5rem; }
.blog-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.375rem; padding: 0.5rem 0 4.75rem; }
.blog-card {
	display: flex; flex-direction: column; padding: 1.5rem 1.5rem 1.375rem; color: inherit; overflow: hidden;
	background: #fff; border: 1px solid var(--hair-l); border-radius: var(--radius-lg);
	transition: border-color .2s ease, transform .35s var(--ease), box-shadow .35s;
}
.blog-card:hover { border-color: var(--teal); transform: translateY(-0.3125rem); box-shadow: var(--shadow-soft); }
/* Bleeds to the card edge through the card's own padding, same trick the
   case-study cards use — the card clips it to the corner radius, so the
   media itself needs no radius. Left at the motif's native 12:5 canvas
   (not cropped to 16:9): several motifs run edge to edge — the RAG
   pipeline, the FIX message flow, the TLS handshake — and a tighter crop
   would cut off the payoff on one side. */
.blog-card-media { margin: -1.5rem -1.5rem 1.125rem; aspect-ratio: 12 / 5; overflow: hidden; background: var(--hero-ink); }
.blog-card-cover { display: block; width: 100%; height: 100%; transition: transform .5s var(--ease); }
/* The motif is drawn once at its article-hero scale (~700-900px wide) and
   reused here at under 400px — stroke-width is set in viewBox units, so
   without this every line would shrink proportionally to a sub-pixel
   hairline. Pinning stroke width to screen pixels keeps the same lines
   crisp at both sizes instead of needing a second, thumbnail-specific
   drawing per motif. */
.blog-card-cover * { vector-effect: non-scaling-stroke; }
.blog-card:hover .blog-card-cover { transform: scale(1.04); }
/* The label, the slash and the reading time each stay in one piece; when the card is too
   narrow for the whole line ("SECURITY & CRYPTOGRAPHY / 8 MIN READ" needs ~324px, a desktop
   card has 306px) the reading time drops to a second line instead of splitting mid-word. */
.blog-card-meta {
	display: flex; flex-wrap: wrap; align-items: center; gap: 0.25rem 0.625rem; font-family: var(--font-mono); font-size: var(--text-min);
	letter-spacing: .12em; text-transform: uppercase; color: var(--blog-quiet); margin-bottom: 0.8125rem;
}
.blog-card-meta b { font-weight: 500; color: var(--teal-deep); }
.blog-card-meta i { font-style: normal; opacity: .45; }
.blog-card-meta > * { white-space: nowrap; }
/* On a phone nearly every card is too narrow for the whole line, so all of them stack: the same
   two lines everywhere, and no slash left hanging at the end of the first. */
@media (max-width: 30em) { /* 480px */
	.blog-card .blog-card-meta { flex-direction: column; align-items: flex-start; gap: 0.25rem; }
	.blog-card .blog-card-meta i { display: none; }
}
.blog-card h3 {
	font-family: var(--font-display); font-variation-settings: 'wdth' 100, 'wght' 700;
	font-size: 1.16rem; line-height: 1.22; letter-spacing: -.012em; color: var(--ink); margin-bottom: 0.625rem;
}
.blog-card p {
	font-size: .92rem; line-height: 1.55; color: var(--ink-70); margin-bottom: 1rem; flex: 1;
	display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.blog-card .card-link { color: var(--teal-deep); margin-top: auto; }
@media (max-width: 61.25em) { /* 980px */ .blog-grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 43.75em) { /* 700px */ .blog-grid { grid-template-columns: 1fr; } }
@media (prefers-reduced-motion: reduce) {
	.blog-card-cover { transition: none; }
	.blog-card:hover .blog-card-cover { transform: none; }
}

/* --- topic row and topic pages ----------------------------------------------
   A topic page (/insights/blog/topics/<topic>/) is the blog index narrowed to one topic:
   the same hero, the same cards and the same grid, so nothing here restyles
   the index. The row of topics sits under the hero on the index and on every
   topic page, and is how a reader moves between them. The current one is
   filled, the others outlined. On a wide screen the row wraps onto a second
   line; on a phone it scrolls sideways (see below). */
.blog-index-hero .blog-title { margin-top: 0.875rem; font-size: clamp(1.9rem, 4vw, 2.7rem); }
.blog-card-meta a, .blog-meta b a {
	text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 0.25em;
	text-decoration-color: rgba(47,126,142,.4);
}
.blog-card-meta a { font-weight: 500; color: var(--teal-deep); }
.blog-card-meta a:hover, .blog-meta b a:hover { text-decoration-color: currentColor; }
.topic-nav { display: flex; flex-wrap: wrap; gap: 0.5rem; margin: 1.5rem 0 0.25rem; }
.topic-nav a {
	display: inline-flex; align-items: center; gap: 0.5rem; min-height: 2.5rem; padding: 0.4375rem 1rem;
	background: #fff; border: 1px solid var(--hair-l); border-radius: 999px;
	font-size: .9rem; font-weight: 500; line-height: 1.2; color: var(--ink-70);
	transition: border-color .2s ease, color .2s ease, background-color .2s ease;
}
.topic-nav a span { font-family: var(--font-mono); font-size: var(--text-min); color: var(--blog-quiet); }
.topic-nav a:hover { border-color: var(--teal); color: var(--ink); }
.topic-nav a[aria-current] { background: var(--ink); border-color: var(--ink); color: #fff; }
.topic-nav a[aria-current] span { color: rgba(255,255,255,.72); }
.topic-nav a:focus-visible { border-radius: 999px; }
/* On a phone the row scrolls sideways instead of wrapping into six lines ahead of the first
   article. It bleeds to the screen edge through the container's 1.5rem gutter, and
   initTopicNav() in main.js scrolls the current topic into view. */
@media (max-width: 43.75em) { /* 700px */
	.topic-nav a { min-height: 2.75rem; flex: none; }
	.topic-nav {
		position: relative; flex-wrap: nowrap; overflow-x: auto; margin: 1.125rem -1.5rem 0; padding: 0.375rem 1.5rem;
		scroll-padding-inline: 1.5rem; scrollbar-width: none; -webkit-overflow-scrolling: touch;
	}
	.topic-nav::-webkit-scrollbar { display: none; }
}


@media (max-width: 68.75em) { /* 1100px */
	.blog-layout { grid-template-columns: minmax(0, 1fr); }
	/* width: 100% -- see the comment on the :not(.has-rail) version of this
	   rule above; same fix, same reason, for posts that do have a rail. */
	.blog-layout.has-rail .blog-column { width: 100%; max-width: 45rem; margin: 0 auto; }
	/* The rail becomes a flat, non-sticky "in this article" block instead of
	   a sidebar — a scroll-spy sidebar doesn't fit next to a single-column
	   reading measure, but the section list is still useful up front. */
	.blog-rail { position: static; order: -1; max-width: 45rem; margin: 0 auto 0.5rem; padding: 1.25rem 1.375rem; background: var(--paper); border-radius: var(--radius); }
	.blog-rail ol { columns: 2; column-gap: 1.5rem; }
	.blog-rail li { break-inside: avoid; }
	.blog-layout.has-svc-rail { grid-template-columns: minmax(0, 1fr); }
	/* Same trade as .blog-rail above: the sliding indicator is a sticky-scroll
	   effect that doesn't fit next to a single-column reading measure, so it
	   drops out and this becomes a plain flat jump list. */
	.svc-rail { position: static; max-width: 45rem; margin: 0 auto 0.5rem; }
	.svc-rail ol { columns: 2; column-gap: 1.5rem; }
	.svc-rail-item { break-inside: avoid; }
	.svc-rail-indicator { display: none; }
}
@media (max-width: 45em) { /* 720px */
	.blog-title { max-width: none; }
	.blog-rail ol { columns: 1; }
	.svc-rail ol { columns: 1; }
	.blog-code-wrap pre { padding: 1rem; }
	.blog-code-wrap code { font-size: .8rem; }
	.blog-callout { padding: 1.5rem 1.375rem; }
	.blog-next-card { padding: 1.375rem 1.25rem; }
	.svc-feature { grid-template-columns: 1fr; }
	/* h2 -> lead -> visual -> list, same technique as .capability --
	   .svc-feature--illus only (Cloud Engineering, AI Native Product
	   Engineering). Both use a labelled technical diagram that reads as
	   real information and holds up sitting mid-copy. The slot is square
	   here, the same shape as the illustrations themselves: it was 4:3, and
	   object-fit: cover then trimmed 12.5% off the top and the bottom of
	   every one. Six of the seven draw to within 0-6% of those edges, so
	   their labels and icons were cut ("OFFLINE DATA CACHING" read "DATA
	   CACHING", Azure lost its logo). A square slot crops nothing, and the
	   bottom fade shrinks to the last 6% so it only softens the edge. Business Intelligence's plain
	   .svc-feature (no --illus) uses tall, uncropped portrait photography
	   that reads as generic stock imagery instead of a specific diagram --
	   sandwiched between the lead and the list it's a long, contentless
	   scroll in the middle of the argument, so that variant stays
	   media-first below rather than getting the same split. */
	.svc-feature--illus > .svc-feature-copy { display: contents; }
	.svc-feature--illus .svc-feature-h2 { order: 1; padding: 1.875rem 1.625rem 0; }
	.svc-feature--illus .svc-feature-lead { order: 2; padding: 0 1.625rem; }
	.svc-feature--illus .svc-feature-media {
		order: 3; min-height: 0; aspect-ratio: 1 / 1; margin-top: 0; margin-bottom: 1.5rem;
	}
	.svc-feature--illus .svc-feature-list { order: 4; padding: 0 1.625rem 1.875rem; }
	.svc-feature--illus .svc-feature-media::after {
		background: linear-gradient(to bottom, transparent 94%, var(--feature-tint) 100%);
	}
	.svc-feature:not(.svc-feature--illus) .svc-feature-media { order: -1; min-height: 13.75rem; }
	.svc-feature:not(.svc-feature--illus) .svc-feature-copy { padding: 1.875rem 1.625rem; }
}
@media (prefers-reduced-motion: reduce) {
	.blog-next-card { transition: none; }
	.blog-next-card:hover { transform: none; }
	.blog-rail li::before, .blog-rail a { transition: none; }
	.svc-rail-item a, .svc-rail-indicator { transition: none; }
}

/* ==========================================================================
   Products — two, on one shelf

   Everything under Resources and Services describes work done for someone
   else; these didn't start as marketing copy — they started as internal
   tools that got good enough to ship on their own. The index reuses the
   blog/case-study index's card-with-cover-art frame, but both covers share
   one gradient rather than a different palette each: the tell that these
   belong to the same shelf, distinguished only by the glyph drawn on it.
   ========================================================================== */
.products { --product-quiet: rgba(16,22,25,.64); }
/* .product-hero's own padding was 4px 0 8px, leaning entirely on #primary's
   sitewide 84px padding-top to clear the fixed header -- true on pages
   whose header sits at its shortest (.is-day, 10px 0 + 9px container
   padding, ~55px tall), but this page has no listed dark hero (see the
   #primary:has(...) rule list), so at first paint -- before any scroll
   has happened -- the header is always at its tall default (20px 0 + 12px
   container padding, ~115px), well past the 84+4=88px this was giving
   the docket line. 108px replaces that 4px to clear the tall header at
   load. Since fixed, this page's header now also shrinks once scrolled
   past DOCK_AT (initHeroHeader() in main.js runs its generic .is-day
   toggle on every page, not just the dark-hero list) -- but that happens
   well after load, once the hero itself has scrolled out from under the
   header, so it doesn't change what this padding needs to clear. */
.product-hero { padding: 6.75rem 0 2.5rem; }
.product-docket {
	display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem 0.875rem; margin-bottom: 1.25rem;
	font-family: var(--font-mono); font-size: var(--text-min); letter-spacing: .14em;
	text-transform: uppercase; color: var(--product-quiet);
}
.product-docket b { font-weight: 500; color: var(--teal-deep); }
.product-docket i { font-style: normal; opacity: .4; }
.product-title {
	font-family: var(--font-display); font-variation-settings: 'wdth' 84, 'wght' 760;
	font-size: clamp(2.15rem, 5.4vw, 3.6rem); line-height: 1.05; letter-spacing: -.021em;
	max-width: 20ch; color: var(--ink);
}
.product-lede { margin-top: 1.25rem; max-width: 60ch; font-size: 1.14rem; line-height: 1.56; color: var(--ink-70); }

.product-index { padding: 2.75rem 0 4.75rem; }
.product-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1.625rem; }
.product-card {
	display: flex; flex-direction: column; padding: 1.875rem 1.875rem 1.625rem; color: inherit; overflow: hidden;
	background: #fff; border: 1px solid var(--hair-l); border-radius: var(--radius-lg);
	transition: border-color .2s ease, transform .35s var(--ease), box-shadow .35s;
}
.product-card:hover { border-color: var(--teal); transform: translateY(-0.3125rem); box-shadow: var(--shadow-soft); }
/* Bleeds to the card edge through the card's own padding, same trick the
   blog/case-study cards use — the card clips it to the corner radius. */
.product-card-media { margin: -1.875rem -1.875rem 1.375rem; aspect-ratio: 16 / 9; overflow: hidden; }
/* Dark frame, real evidence inside — replaces the shared abstract-gradient
   cover both cards used to draw the same way ("distinguished only by the
   glyph"). Both products are real and different from each other, so now
   both media panels show that directly: Nallai's own screenshot, BI
   Migration's own hero component. The "shelf" consistency the original
   design named comes from the dark frame both sit in, not from a shared
   illustration standing in for either. */
.product-card-media--dark {
	display: flex; align-items: center; justify-content: center;
	background: #101619; padding: 1.375rem;
}
/* Nallai's card media — a real screenshot read as an awkward crop of
   someone else's list view (a portrait UI forced into a 16:9 box). Redrawn
   instead: an original hexagon cluster in Nallai's own warm coral/gold —
   not a copy of their logo, but the same device their product itself
   uses for a self-organizing map, which is what the card copy right below
   it already names. Nothing to license — original artwork, not sourced. */
.product-illustration { width: 100%; height: 100%; display: block; }
.product-illustration .hex { transition: transform .5s var(--ease); transform-box: fill-box; transform-origin: center; }
.product-card:hover .product-illustration .hex { transform: scale(1.04); }
@media (prefers-reduced-motion: reduce) {
	.product-card:hover .product-illustration .hex { transform: none; }
}
/* Self-contained rather than reusing BI Migration's own .migration-ticket
   component from design/bi-migration-redesign -- that branch isn't merged
   yet, and this page shouldn't depend on another feature branch's classes
   existing. Same visual idea (a small ticket showing the real three-stage
   pipeline mid-run), independently defined. */
.product-preview {
	width: 15rem; padding: 1.125rem; border-radius: 0.625rem;
	background: rgba(234,243,241,.04); border: 1px solid rgba(234,243,241,.12);
	font-family: var(--font-mono); font-size: var(--text-min); color: rgba(234,243,241,.55);
}
/* Wraps instead of squeezing: when the 15rem card can't fit the title and
   the route side by side, the route drops under the title rather than the
   title breaking into a word per line. */
.preview-head {
	display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 0.25rem 0.5rem;
	padding-bottom: 0.6875rem; border-bottom: 1px solid rgba(234,243,241,.12);
	font-size: var(--text-min); color: #EAF3F1; font-weight: 500;
}
.preview-route { color: var(--teal); font-size: var(--text-min); white-space: nowrap; }
.preview-steps { margin-top: 0.8125rem; display: flex; flex-direction: column; gap: 0.25rem; }
.preview-step { display: flex; align-items: center; gap: 0.5rem; padding: 0.3125rem 2px; }
.preview-step-dot {
	flex: none; width: 0.4375rem; height: 0.4375rem; border-radius: 50%;
	border: 1.5px solid rgba(234,243,241,.3); background: transparent;
}
.preview-step.is-done .preview-step-dot { background: #7BE0A8; border-color: #7BE0A8; }
.preview-step.is-current .preview-step-dot { background: var(--butter); border-color: var(--butter); }
.preview-step b { color: #EAF3F1; font-weight: 600; }
.preview-step span { color: rgba(234,243,241,.4); margin-left: 0.3125rem; }
.product-card-tag {
	font-family: var(--font-mono); font-size: var(--text-min); letter-spacing: .14em;
	text-transform: uppercase; color: var(--product-quiet);
}
.product-card h3 {
	margin: 0.75rem 0 0.625rem; font-family: var(--font-display);
	font-variation-settings: 'wdth' 86, 'wght' 720; font-size: 1.55rem;
	line-height: 1.15; letter-spacing: -.018em; color: var(--ink);
}
.product-card p { font-size: 1rem; line-height: 1.6; color: var(--ink-70); margin-bottom: 1.125rem; flex: 1; }
.product-card .card-link { margin-top: auto; color: var(--teal-deep); }
@media (max-width: 43.75em) { /* 700px */ .product-grid { grid-template-columns: 1fr; } }

/* ==========================================================================
   Services mega-menu — 7 practices grouped into 4 buying-motion families
   (Applied AI / Engineering / Assurance / Teams), one column per family,
   each practice inside listing its own sub-services. Same hover/focus
   triggers as the base .sub-menu (initDropdownNav doesn't care what's
   inside). Replaces an earlier 7-columns-plus-promo-tile layout where
   .sub-menu--mega set display:grid without resetting the align-items:center
   it inherited from .primary-nav ul, so every column centred inside its row
   track instead of sharing a top edge — worst on Staff Augmentation, the
   one practice with no sub-services, which floated ~97px below its
   neighbours. Full rationale and the two other directions this was chosen
   from: docs/design/services-dropdown-explorations.html (that review still
   shows a proof-stats footer band, since dropped from the shipped version).
   ========================================================================== */
.primary-nav .sub-menu--mega {
	/* left is a plain px value JS overwrites on every open (positionMegaPanel
	   in main.js) — centering purely on the trigger's own position overflowed
	   off the left edge of the screen at ~720-1145px widths, taking the
	   Applied AI / Engineering columns off-screen with it, so JS measures the
	   real viewport and clamps it inside a 16px gutter instead.
	   left is deliberately a plain, untransitioned property, not folded into
	   the `transform` the base .sub-menu rule already animates (translateY,
	   inherited below — same value on this variant, so it isn't repeated
	   here). It used to be: translateX(calc(-50% + var(--mega-shift))),
	   baked into that same transitioned transform. Because `transform` is
	   what `transition: transform` animates, updating the --mega-shift
	   custom property in the same tick as the .is-open class (which is what
	   starts that transition) raced the browser's own transition
	   retargeting — the slide would start before the corrected shift was
	   captured and then never pick it up, so every *second* open rendered at
	   the stale (usually badly off-screen) position while the first open,
	   with no prior transition to interrupt, happened to read correctly.
	   Plain `left` sidesteps that race entirely. */
	left: 0; min-width: 0; width: min(67.5rem, calc(100vw - 3rem)); padding: 0;
}

/* .primary-nav .sub-menu--mega .mega-families (3 classes), not just
   .mega-families (1 class) — every <ul> inside .primary-nav also matches
   the base ".primary-nav ul { display:flex; align-items:center }" rule,
   which at (0,1,1) beats a bare single-class selector at (0,1,0). Same fix
   as .sub-menu--mega itself needed against that same base rule; without it
   this grid rendered as a centred flex row instead, the same alignment bug
   this whole redesign exists to fix, one element deeper. */
.primary-nav .sub-menu--mega .mega-families {
	display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); align-items: start; gap: 0;
	/* gap:0 explicitly — display:grid wins the cascade on specificity, but
	   the base ".primary-nav ul { gap:30px }" rule still wins for the `gap`
	   property alone since nothing here contested it, eating 90px across
	   3 column gaps and narrowing every family by ~23px. */
	padding: 1.5rem 0.25rem 1.375rem;
}
.mega-family { padding: 0 0.9375rem; border-left: 1px solid var(--hair-l); min-width: 0; }
.mega-family:first-child { border-left: 0; }
.mega-family-label {
	display: flex; align-items: center; gap: 0.4375rem; margin-bottom: 0.875rem;
	font-family: var(--font-mono); font-size: var(--text-min); letter-spacing: .16em;
	text-transform: uppercase;
	/* --ink-45 measures ~3.06:1 on the white panel — under the 4.5:1 floor
	   for normal-size text. --ink-70 (already used for the top-level nav
	   links) clears it at ~6.76:1. */
	color: var(--ink-70);
}
/* Colour set per family via an inline custom property at each .mega-family,
   not by :nth-child position — the old per-column :nth-child(1)..(7) rules
   meant reordering or inserting a practice silently reshuffled every
   colour below it. */
.mega-family-label::before { content: ''; width: 0.4375rem; height: 0.4375rem; border-radius: 2px; background: var(--rail); flex: none; }
.mega-practice + .mega-practice { margin-top: 1.125rem; }
/* Qualified with .primary-nav .sub-menu--mega (not just .mega-practice-link)
   so this beats .primary-nav .sub-menu a's white-space:nowrap on
   specificity — without that, every practice title stays one unbroken
   line and blows out the grid's 1fr columns to fit it. */
.primary-nav .sub-menu--mega .mega-practice-link {
	display: flex; align-items: flex-start; gap: 0.5625rem; padding: 0.3125rem 0.5rem 0.3125rem 0.5625rem; margin-left: -0.5625rem;
	border-radius: 0.5rem; position: relative;
	font-family: var(--font-display); font-variation-settings: 'wdth' 108, 'wght' 700;
	font-size: 0.90625rem; line-height: 1.25; color: var(--ink); letter-spacing: -.005em; white-space: normal;
	transition: background .16s ease;
}
.mega-practice-link:hover { background: var(--accent); }
.mega-practice-link .mega-ico { flex: none; width: 1rem; height: 1rem; margin-top: 1px; color: var(--rail); }
/* The practice you're already on (marked current-menu-item + aria-current
   per-page — this is a static export, nothing computes it at request time)
   gets a quiet, permanent version of the hover state rather than a loud
   highlight box: the message is "you're already here", not "click me". Text
   and icon pick up --teal-deep so it still reads without a mouse anywhere
   near it — the link's own default color is already full ink, same as
   every other practice, so background alone wouldn't be enough signal. */
.mega-practice.current-menu-item > .mega-practice-link { background: var(--accent); color: var(--teal-deep); }
.mega-practice.current-menu-item > .mega-practice-link .mega-ico { color: var(--teal-deep); }
/* Same fix as .mega-families above — .mega-family-list and .mega-sublist
   are also <ul> elements inside .primary-nav, so both need display:block
   restored against the inherited flex row or the practices and their
   sub-services render side by side instead of stacked. */
.primary-nav .sub-menu--mega .mega-family-list { display: block; }
.primary-nav .sub-menu--mega .mega-sublist { display: block; margin-top: 0.3125rem; padding-left: 1.5625rem; }
.primary-nav .sub-menu--mega .mega-sub {
	display: block; padding: 0.3125rem 0.375rem; margin-left: -0.375rem; border-radius: 0.4375rem;
	font-size: var(--text-min-sans); line-height: 1.35; color: var(--ink-70); white-space: normal;
	transition: background .16s ease, color .16s ease;
}
.primary-nav .sub-menu--mega .mega-sub:hover { background: var(--accent); color: var(--teal-deep); }

@media (max-width: 45em) { /* 720px */
	.primary-nav .sub-menu--mega { display: block; }
	/* .primary-nav .sub-menu--mega .mega-families, not bare .mega-families —
	   same specificity issue as the desktop rule needed fixing against
	   .primary-nav ul: a bare single-class selector here loses to the
	   3-class desktop grid rule regardless of media-query/source order, so
	   the grid never actually switched to a block stack on mobile without
	   this prefix. */
	.primary-nav .sub-menu--mega .mega-families { display: block; padding: 0; }
	.mega-family { border-left: 0; padding: 0; margin-bottom: 1rem; }
	/* Same selector/specificity as the desktop flex rule above, so this wins
	   the cascade tie purely by appearing later — needed because the mobile
	   nav's own ".primary-nav a { display:block }" rule ties in specificity
	   with ".sub-menu--mega .mega-practice-link" but loses on source order. */
	.primary-nav .sub-menu--mega .mega-practice-link { padding-top: 0.625rem; padding-bottom: 0.625rem; }
}

/* Services hub cards — sub-service links under the pitch, above the
   category card-link. Reuses .card as-is; this is the only addition. */
.card-sublinks { list-style: none; margin: 0.875rem 0 0; padding: 0.75rem 0 0; border-top: 1px solid var(--hair-l); display: flex; flex-direction: column; gap: 0.4375rem; }
.card-sublinks a, .card-sublinks--static li { display: flex; align-items: center; gap: 0.5rem; font-size: .86rem; color: var(--ink-70); }
.card-sublinks a::before, .card-sublinks--static li::before { content: ''; width: 0.25rem; height: 0.25rem; border-radius: 50%; background: var(--teal); flex: none; }
.card-sublinks a:hover { color: var(--teal-deep); }

/* ==========================================================================
   Agentic AI service page

   The default .page-hero (centred icon, h1, paragraph on paper) leaves the
   top of a service page reading as empty, so this page opens on the shared
   dark .photo-hero instead — same component the industry pages use.
   ========================================================================== */

/* --- signature: the twin runs --------------------------------------------
   The one thing this page has to land is that an agent either finishes by
   itself or stops and asks — and that *which* path a run takes is a setting
   you control, not a surprise. So rather than one run waterfall shown in
   isolation, the same claims agent is shown twice on a shared claim: one
   run clears every check and settles in ~2s, the other holds for an
   adjuster and takes minutes. Each card is headed by its path's name in
   display type, teal for settle and butter for hold. These cards used to
   lead with the insurer case study's 78/22 split; that case study was
   taken off the site (2026-10-22), and an unsourced percentage would read
   as a measured result, so the path itself now leads. */
.twin-runs { display: grid; grid-template-columns: 1fr 1fr; gap: 1.625rem; align-items: stretch; }
.twin-run { display: flex; flex-direction: column; gap: 1rem; }
.twin-run-head { display: flex; align-items: center; gap: 0.75rem; }
.twin-run-label { font-size: .95rem; line-height: 1.4; color: var(--lite-72); }
.twin-run-label b {
	display: block; margin-bottom: 0.25rem;
	font-family: var(--font-display); font-variation-settings: 'wdth' 104, 'wght' 720;
	font-size: clamp(1.25rem, 2vw, 1.45rem); line-height: 1.15; letter-spacing: -.01em;
	color: var(--teal);
}
.twin-run--hold .twin-run-label b { color: var(--butter); }
/* .card-run is a flex column that stretches; inside a fixed grid row both
   cards then match height regardless of row count. */
.twin-run .card-run { flex: 1; }
.twin-run-verdict {
	display: flex; align-items: center; gap: 0.5625rem; padding: 0.8125rem 1rem;
	border: 1px solid var(--border); border-radius: 0.75rem;
	font-family: var(--font-mono); font-size: var(--text-min); line-height: 1.5; color: var(--lite-72);
}
.twin-run-verdict svg { flex: none; width: 0.9375rem; height: 0.9375rem; color: var(--teal); }
.twin-run--hold .twin-run-verdict svg { color: var(--butter); }

/* --- inline diagrams inside the deep-dive column -------------------------
   The reading column is 720px, so these are drawn to sit at that width
   rather than being pulled full-bleed — which would break the rail's
   scroll-spy alignment for the sake of a wider picture. */
.svc-diagram {
	margin: 1.625rem 0 0; padding: 1.375rem; border-radius: var(--radius);
	background: var(--hero-ink); border: 1px solid var(--hair-l);
}
/* Scoped to the scroll wrapper, not to .svc-diagram as a whole — the checks
   panel's head carries a 14px inline icon that a bare `.svc-diagram svg`
   rule would stretch to the full column width. */
.svc-diagram-scroll svg { display: block; width: 100%; height: auto; }
/* These are drawn on a 700-unit canvas with 10-12px labels. Scaling the
   whole SVG down to a phone's width would take that type to ~4px, so below
   the point where it stays readable the diagram keeps its size and scrolls
   in its own track instead — the page itself never scrolls sideways. */
.svc-diagram-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }
/* A grid item defaults to min-width:auto, so .blog-column refuses to shrink
   below its widest child and pushes past its own track instead — which
   body's overflow-x:clip then hides rather than scrolls. Letting it shrink
   to the track is what allows the wrapper above (and .blog-code-wrap on
   long posts) to scroll its own overflow instead of losing it off-screen. */
.blog-layout .blog-column { min-width: 0; }
/* Only below the width where the 700-unit canvas would take its 10-12px
   labels under ~9px does the diagram stop scaling and start scrolling.
   The floor is set on the wrapper from the viewport rather than as a
   min-width on the SVG: a min-width there feeds back into the reading
   column's own min-content size and pushes the whole column — body text
   included — out under overflow-x:clip. A vw-derived max-width can't,
   because it never depends on the content inside it. */
@media (max-width: 45em) { /* 720px */
	.svc-diagram-scroll { max-width: calc(100vw - 5.75rem); }
	.svc-diagram-scroll svg { min-width: 32.5rem; }
}
.svc-diagram figcaption {
	margin-top: 0.875rem; padding-top: 0.8125rem; border-top: 1px solid rgba(234,243,241,.14);
	font-family: var(--font-mono); font-size: var(--text-min); line-height: 1.5; color: rgba(234,243,241,.55);
}

/* Guardrail panel — a dark inset list for the evals section, where the
   argument is a list of checks that either pass or hold. Its head is an icon
   and a mono caps label over a hairline. */
.svc-checks-head {
	display: flex; align-items: center; gap: 0.4375rem; padding-bottom: 0.8125rem; margin-bottom: 1rem;
	border-bottom: 1px solid var(--border); font-size: var(--text-min); letter-spacing: .1em;
	text-transform: uppercase; color: rgba(234,243,241,.5);
}
.svc-checks-head svg { flex: none; }
.svc-checks { margin: 1.625rem 0 0; }
.svc-check {
	display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 0.75rem;
	padding: 0.75rem 0; border-bottom: 1px solid rgba(234,243,241,.12);
}
.svc-check:last-child { border-bottom: 0; }
.svc-check-dot { width: 0.5rem; height: 0.5rem; border-radius: 50%; background: #7BE0A8; flex: none; }
.svc-check--hold .svc-check-dot { background: var(--butter); }
.svc-check-name { font-size: 0.78125rem; color: #EAF3F1; }
.svc-check-state { font-family: var(--font-mono); font-size: var(--text-min); color: #7BE0A8; white-space: nowrap; }
.svc-check--hold .svc-check-state { color: var(--butter); }

@media (max-width: 53.75em) { /* 860px */
	.twin-runs { grid-template-columns: 1fr; gap: 2rem; }
}

/* ==========================================================================
   Industry pages (/industries/quant-finance/, /payments/, /health-insurance/)
   — duotone photography + instrument-panel diagrams.

   The rest of the site deliberately skips stock photography for a bespoke
   "instrument panel" visual language (see the practice-cover comment on the
   services hub). These three pages are the one place real photographs
   appear, by design — but run through a brand-tinted duotone rather than
   full colour, so a photo of a trading screen or a card reader still reads
   as part of the same dark-ink, teal/cobalt/persimmon system instead of a
   generic stock-photo insert. Each page sets its own --duo-a/--duo-b pair
   inline to get a distinct tint without three near-duplicate rule sets.
   ========================================================================== */
.photo-hero {
	--background: var(--hero-ink); --foreground: #EAF3F1; --surface: var(--hero-ink);
	--card: var(--hero-ink-raise); --muted-foreground: var(--lite-72);
	--accent: rgba(43,52,224,.16); --border: rgba(234,243,241,.14);
	background: var(--background); color: var(--foreground);
	padding: 9.625rem 0 5rem; position: relative; overflow: hidden;
}
.photo-hero::before {
	content: ''; position: absolute; inset: -20% -10% auto -10%; height: 38.75rem;
	background: var(--gradient-aurora); opacity: .38; filter: blur(50px); pointer-events: none;
}
.photo-hero .container { position: relative; z-index: 1; }

/* Live network background (Agentic AI) — same canvas system as
   .genai-hero.is-live-network, spanning .agentic-atmosphere (the hero and
   the twin-runs comparison; the client-result proof card sits outside
   this zone, back on daylight). Both the node network and the breathing
   aurora glow fill the full wrapper height. Trialled and tuned on
   /services/agentic-ai-design-test/ before landing here.

   overflow:hidden below is load-bearing, not tidiness — the canvas is
   sized once from JS at load, and a web-font swap reflowing text after
   that can leave the wrapper's true height a little shorter than what
   the canvas already committed to. Without a clip, that last sliver of
   canvas — right where the persimmon aurora blob sits (cy 0.98) — bled
   out under the wrapper's edge and over the proof card below. The 56px
   top margin on .svc-proof is deliberate spacing on top of that clip,
   not a replacement for it: a dark, animated zone and a plain daylight
   card should read as two beats, not one bleeding into the other. */
.agentic-atmosphere { position: relative; background: var(--hero-ink); overflow: hidden; }
.agentic-atmosphere .container { position: relative; z-index: 1; }
.photo-hero.is-live-network { background: transparent; }
.photo-hero.is-live-network::before { display: none; }
.agentic-atmosphere .section--night { background: transparent; }
#agentic-network-bg { position: absolute; inset: 0; z-index: 0; pointer-events: none; display: block; }
/* Agentic AI is the one service hero built on .photo-hero, the industry and
   company family, whose headline runs a step larger (3.5rem). The seven
   other service heroes, Gen AI included, use 2.25-3.4rem; this puts Agentic
   AI on their scale without touching the pages that share .photo-hero. The
   photo is a landscape picture, framed 4:3 like the Gen AI hero's. */
.agentic-atmosphere > .photo-hero .hero-copy h1 { font-size: clamp(2.25rem, 4.3vw, 3.4rem); }
.agentic-atmosphere > .photo-hero .photo-duo { aspect-ratio: 4 / 3; }
.svc-proof { margin-top: 3.5rem; }

/* Industry family hero wrapper (docs/design/industry-family-design.md) —
   each of the three industry pages (quant-finance, payments, health-
   insurance) gets a static canvas, not a .is-live-network one: settles once
   on load, then never runs again. Not given the is-live-network class since
   that name promises an ongoing simulation these canvases deliberately
   don't run — same transparent-background/no-aurora effect, spelled out
   under this shared family selector instead of borrowing a name that would
   misdescribe it. Health Insurance ("the triage field") is the reference
   implementation; each page's canvas keeps its own id + inline script for
   its own field concept, same as every other canvas hero on this site
   (#agentic-network-bg, #cloud-hero-network-bg, …) — only the wrapper and
   the background suppression above are shared family-level rules. */
.ind-hero.photo-hero { background: transparent; }
.ind-hero.photo-hero::before { display: none; }
#ind-hero-triage-bg { position: absolute; inset: 0; z-index: 0; pointer-events: none; display: block; }
#ind-hero-pulse-bg { position: absolute; inset: 0; z-index: 0; pointer-events: none; display: block; }
#ind-hero-tape-bg { position: absolute; inset: 0; z-index: 0; pointer-events: none; display: block; }

/* Found rolling out Payments: .hero-stats' generic repeat(4, auto) grid
   sizes each column to its own content, but a browser's default line-
   breaking treats a hyphen as a soft-wrap point even when the word would
   otherwise fit -- CSS Grid's automatic minimum track size for text is
   min-content, and the min-content width of a hyphenated word is just its
   longest unbroken segment ("PCI-" or "DSS"), not the whole word. That
   collapsed PCI-DSS's own column and forced it to two lines at 1440px
   while its three neighbours (no hyphen) stayed on one -- measured
   (82px/2 lines vs 41px/1 line), not assumed. Scoped to the family rather
   than just Payments: Quant Finance's "FRM/CFA" carries the same risk at
   a narrower viewport even though it doesn't trip it today. */
.ind-hero .hero-stats strong { white-space: nowrap; }

/* The industry family's headline runs a step under the shared .photo-hero
   one (3.5rem, still used by the company pages). At 3.5rem Payments' and
   Health Insurance's titles ran to five lines and put the hero CTA below
   the fold at 1536x727. 3.1rem is the floor: it matches the closing
   .cta-band h2 and stays above .section-head h2 (2.9rem), so the title is
   never smaller than a heading further down the page. The extra class
   outranks .photo-hero .hero-copy h1, which comes later in this file. */
.ind-hero.photo-hero .hero-copy h1 { font-size: clamp(2.1rem, 4vw, 3.1rem); }

/* Phase rail upgrade (Agentic AI only) — .svc-phase/.svc-phases are
   shared with 6+ other pages (staff-augmentation, cloud-engineering,
   three industry pages), so this lives under a .svc-phases--rich
   modifier rather than changing the bare rules those pages still use
   unmodified. */
.svc-phases--rich { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.75rem; margin-bottom: 0.5rem; }
.svc-phases--rich::before { display: none; }
.svc-phases--rich .svc-phase {
	position: relative; background: var(--card); border: 1px solid var(--border);
	border-radius: var(--radius); padding: 1.875rem 1.5rem 1.625rem; overflow: hidden;
	box-shadow: var(--shadow-soft); transition: transform .3s var(--ease), box-shadow .3s var(--ease);
}
.svc-phases--rich .svc-phase:hover { transform: translateY(-0.25rem); box-shadow: var(--shadow-card); }
.svc-phases--rich .svc-phase::before {
	content: ''; position: absolute; top: 0; left: 0; right: 0; height: 0.1875rem;
	background: var(--phase-accent, var(--teal-ink));
}
.svc-phases--rich .svc-phase-num {
	position: relative; width: 2.75rem; height: 2.75rem; border-radius: 50%;
	background: var(--card); border: 1.5px solid var(--phase-accent, var(--teal-ink));
	display: flex; align-items: center; justify-content: center; margin-bottom: 1.125rem;
	font-family: var(--font-display); font-variation-settings: 'wdth' 108, 'wght' 700; font-size: .95rem;
	color: var(--phase-accent, var(--teal-ink));
}
.svc-phases--rich .svc-phase-num::before {
	content: ''; position: absolute; inset: -0.6875rem; z-index: -1; border-radius: 50%;
	background: radial-gradient(circle, var(--phase-accent, var(--teal-ink)), transparent 70%);
	opacity: .2; filter: blur(7px);
}
@media (max-width: 40em) { /* 640px */
	.svc-phases--rich { grid-template-columns: 1fr; gap: 1.25rem; }
}

/* Custom AI Agents media column ran noticeably shorter than its copy
   column (two paragraphs + tech pills + four outcomes vs. one photo),
   leaving a bare gap under the caption. Taller crop plus moving the tech
   pills down here — real content, not filler — closes most of it. */
#custom-ai-agents .capability-media .photo-duo { aspect-ratio: 4 / 3; }
#custom-ai-agents .capability-media .tech-pills { margin: 0.25rem 0 0; }

.photo-hero .breadcrumb a:hover { color: #fff; }
.photo-hero .split { display: grid; grid-template-columns: 1.05fr .95fr; gap: 3.25rem; align-items: center; }
/* .hero-visual carries a -67px top offset tuned for the homepage stage
   hero's own taller copy column (see its definition) — not relevant here,
   and left unset it pulls the photo panel and the instrument panel up out
   of alignment with the copy beside them. .split is only ever paired with
   .hero-visual on these three pages, so this is safe to reset globally
   under that selector rather than needing a per-section override. */
.split .hero-visual { top: 0; }
.photo-hero .hero-copy .eyebrow { color: var(--teal); }
.photo-hero .hero-copy h1 { font-size: clamp(2.3rem, 4.4vw, 3.5rem); }
.photo-hero .hero-lede { margin-top: 1.375rem; max-width: 32.5rem; font-size: 1.12rem; color: var(--lite-72); }
.photo-hero .hero-cta { margin-top: 2.125rem; }
.photo-hero .hero-stats { grid-template-columns: repeat(4, auto); margin-top: 3rem; }

.photo-duo {
	position: relative; border-radius: var(--radius); overflow: hidden;
	aspect-ratio: 4 / 5; border: 1px solid var(--hair-d); box-shadow: var(--shadow-soft);
	background: var(--hero-ink-raise);
}
.photo-duo img { width: 100%; height: 100%; object-fit: cover; display: block; filter: grayscale(1) contrast(1.12) brightness(.92); }
/* The heroes' photos sit in a <picture> (see the hero-visual rule in the 980px
   block of the Responsive section: a hidden <img> is still downloaded, so below
   that width its source is a 1x1 GIF). display: contents takes the wrapper out
   of layout, so the <img> lays out exactly as it did as the container's direct
   child. */
.photo-duo picture { display: contents; }
.photo-duo::before {
	content: ''; position: absolute; inset: 0; z-index: 1; mix-blend-mode: color;
	background: linear-gradient(150deg, var(--duo-a, var(--teal)) 0%, var(--duo-b, var(--beam)) 100%);
	opacity: .8;
}
.photo-duo::after {
	content: ''; position: absolute; inset: 0; z-index: 2;
	background: linear-gradient(190deg, rgba(7,26,32,0) 38%, rgba(7,26,32,.85) 100%);
}
.photo-duo-chip {
	position: absolute; left: 1.125rem; right: 1.125rem; bottom: 1.125rem; z-index: 3;
	background: rgba(7,26,32,.68); backdrop-filter: blur(8px);
	border: 1px solid rgba(234,243,241,.18); border-radius: 0.75rem; padding: 0.8125rem 1rem;
}
.photo-duo-chip-figure {
	font-family: var(--font-display); font-variation-settings: 'wdth' 106, 'wght' 720;
	font-size: 1.3rem; color: #fff; line-height: 1.1;
}
.photo-duo-chip-label { display: block; margin-top: 0.375rem; font-family: var(--font-mono); font-size: var(--text-min); line-height: 1.5; color: rgba(234,243,241,.68); }

/* .photo-duo--natural — lighter processing for photography that's already
   deliberately colored (the About Us AI set: a teal mug, warm desk wood,
   real skin tones) rather than mixed-source stock, where full grayscale +
   an 80%-opacity duotone wash was earning its keep by unifying disparate
   photos into one palette. Full grayscale here just destroyed color that
   was put there on purpose.

   Not a straight removal, though — tested that live against the real
   hero: no filter at all reads as generic corporate stock, and floats as
   a disconnected warm island against the dark hero panel instead of
   belonging to it. This keeps the same mechanism (partial desaturation,
   the existing --duo-a/--duo-b color-blend, a bottom gradient), so the
   photo still ties to the page's palette but the color that makes it
   look real survives. First pass held it at roughly half strength
   (grayscale .4, duotone .38); dialed back again to grayscale .3 /
   duotone .28 once that first pass was live and approved, carrying
   noticeably more of the source photo's own color forward while the
   palette-tie is still visibly there, not just a residual tint.

   Scoped modifier, not a change to .photo-duo itself: this treatment is
   currently About Us only. Every other .photo-duo on the site (services
   heroes, industry heroes, Careers) is untouched and keeps the original
   full treatment until/unless it's rolled out further. */
.photo-duo--natural img { filter: grayscale(.3) contrast(1.05) saturate(1.15); }
.photo-duo--natural::before { opacity: .28; }
.photo-duo--natural::after { background: linear-gradient(190deg, rgba(7,26,32,0) 50%, rgba(7,26,32,.62) 100%); }

/* Open-roles listing — a real job-board row (title, tag/location/type meta,
   one-line blurb, apply button) rather than the plain status text the
   honest "nothing posted" fallback used alone. */
.role-list-note {
	max-width: 40rem; margin: 0 auto 1.5rem; text-align: center;
	font-family: var(--font-mono); font-size: var(--text-min); letter-spacing: .04em;
	color: var(--muted-foreground); font-style: italic;
}
.role-list { list-style: none; margin: 0 auto; padding: 0; max-width: 53.75rem; display: flex; flex-direction: column; gap: 0.875rem; }
.role-card {
	display: flex; align-items: center; justify-content: space-between; gap: 1.5rem;
	padding: 1.5rem 1.75rem; border-radius: var(--radius); background: var(--card); border: 1px solid var(--border);
	transition: border-color .2s ease, transform .2s ease, box-shadow .2s ease;
}
.role-card:hover { border-color: var(--teal); transform: translateY(-0.1875rem); box-shadow: var(--shadow-soft); }
/* The job filters (initJobFilters, main.js) hide non-matching cards by
   setting the `hidden` attribute — but `.role-card` above gives every
   card its own `display: flex`, and an author stylesheet's normal-
   priority display always beats the browser's built-in `[hidden] {
   display: none }` UA rule, regardless of selector specificity. Without
   this, .hidden flips correctly in the DOM (a card's hidden property
   really is true) while the card stays fully visible and laid out —
   the filter looks completely inert even though its logic is correct. */
.role-card[hidden] { display: none; }
.role-card-main h3 { font-size: 1.05rem; margin-bottom: 0.5625rem; }
.role-meta {
	display: flex; flex-wrap: wrap; row-gap: 0.375rem; margin-bottom: 0.625rem;
	font-family: var(--font-mono); font-size: var(--text-min); letter-spacing: .04em;
	text-transform: uppercase; color: var(--teal-deep);
}
.role-meta span:not(:last-child)::after { content: '\00b7'; margin: 0 0.625rem; color: var(--border); }
.role-card-main p { font-size: .92rem; color: var(--muted-foreground); line-height: 1.55; max-width: 56ch; }
.role-card .btn { flex: none; }
@media (max-width: 40em) { /* 640px */
	.role-card { flex-direction: column; align-items: flex-start; gap: 1rem; padding: 1.375rem; }
	.role-card .btn { align-self: stretch; justify-content: center; }
}

/* Industry hub (/industries/) cards — reuses .card--practice's cover/proof/
   sublinks treatment from the services hub wholesale (same padding, same
   hover, same components), but the cover is a real duotone-treated
   photograph instead of an abstract gradient. The distinction is
   deliberate: services are abstract capabilities (gradient fits), industries
   are real-world verticals (a photograph of one fits) — so a visitor who's
   seen both hubs reads the difference before they read a word of copy. */
.practice-cover.is-photo .cover-bg { object-fit: cover; filter: grayscale(1) contrast(1.15) brightness(.82); }
.practice-cover.is-photo::before {
	content: ''; position: absolute; inset: 0; z-index: 1; mix-blend-mode: color;
	background: linear-gradient(135deg, var(--duo-a, var(--teal)) 0%, var(--duo-b, var(--beam)) 100%);
	opacity: .85;
}
.practice-cover.is-photo .practice-cover-icon { z-index: 2; }

/* Industry hub (/industries/) hero — was the plain, centred .page-hero this
   class list still serves ~30 other not-yet-redesigned pages, so this page
   gets its own scoped class instead of a change to the shared one. Its
   padding began as a verbatim copy of .page-hero's (156px top / 64px
   bottom), which counts the fixed header's clearance twice: #primary's own
   84px of padding-top plus the 156px here put the eyebrow ~165px under the
   header on a phone (~154px on a tablet), against the ~60px every dark hero
   in the family leaves. Same fix those heroes use: #primary stands down and
   the hero's own top padding (152px; 135px under 861px) carries the
   clearance alone. The bottom is trimmed as well -- the next .section adds
   its own 96px (64px on a phone) on the same background, so the hero only
   has to keep the gauge's caption clear of it. */
.industry-hero { padding: 9.5rem 0 2rem; }
#primary:has(> .industry-hero) { padding-top: 0; }
.industry-hero-grid { display: grid; grid-template-columns: 1.3fr .7fr; gap: 2.75rem; align-items: center; }

/* The threshold — replaces "the proof fan" (three duotone photo tiles,
   kept in git history not here): that version echoed the three photo
   cards directly below at smaller scale, and read as a repeat of the body
   rather than a different design for the hero — critiqued directly and
   agreed on rather than kept and reskinned, since a hero and its body
   shouldn't say the same thing in the same shape. This states the page's
   own lede once, as a single picture, instead of indexing the three items
   the body already indexes: a calibrated gauge, needle resting exactly on
   the line between "caution" and "critical" — the sectors where getting
   the details, and the AI, wrong costs the most are the ones where normal
   operation sits closer to that line than anyone would like. The three
   real stats moved into .hero-stats at first — an established component,
   reused verbatim rather than reinvented — but review caught that the same
   three numbers are also each card's own proof-callout figure below, and
   here (unlike on a single-subject detail page, where hero-stats always
   sit under an h1 that already names the one subject they belong to) nothing
   in the hero says which industry each number is even about. Cut rather
   than fixed: the hero states the thesis, the cards are where each number
   properly lives, next to its own industry name, paragraph and case-study
   link. See Revision 2 in industry-hub-design.md. */
.threshold-gauge { display: flex; flex-direction: column; align-items: center; justify-content: center; }
.threshold-gauge svg { width: 100%; max-width: 21.25rem; height: auto; overflow: visible; }
.gauge-band { fill: none; stroke-width: 20; stroke-linecap: butt; }
.gauge-band--safe { stroke: var(--teal); }
.gauge-band--caution { stroke: var(--butter); }
.gauge-band--critical { stroke: var(--sun); }
.gauge-ticks line { stroke: var(--ink); }
.tick--minor { stroke-opacity: .28; stroke-width: 2; }
.tick--major { stroke-opacity: .5; stroke-width: 2.5; }
.needle-main { stroke: var(--ink); stroke-width: 4; stroke-linecap: round; }
.needle-tail { stroke: var(--ink); stroke-width: 3; stroke-linecap: round; }
.needle-hub { fill: var(--ink); }
.needle-hub-dot { fill: var(--paper-hi); }
/* Legend + caption — the gauge read as striking but silent: nothing told
   a sighted visitor what the three colours meant or why the needle sits
   where it does (a screen-reader user got the full explanation via the
   SVG's own aria-label; nobody else did). The dots reuse the band's own
   three colours directly, not a second palette, so the key is provably
   the same key the illustration itself uses. */
.gauge-legend { display: flex; align-items: center; justify-content: center; gap: 1.125rem; margin-top: 0.875rem; }
.gauge-legend-item { display: flex; align-items: center; gap: 0.375rem; font-family: var(--font-mono); font-size: var(--text-min); letter-spacing: .04em; color: var(--muted-foreground); }
.gauge-legend-item i { display: block; width: 0.4375rem; height: 0.4375rem; border-radius: 50%; flex: none; font-style: normal; }
.gauge-legend-item--safe i { background: var(--teal); }
.gauge-legend-item--caution i { background: var(--butter); }
.gauge-legend-item--critical i { background: var(--sun); }
.gauge-caption { margin-top: 0.75rem; max-width: 18.75rem; text-align: center; font-size: .86rem; line-height: 1.5; color: var(--muted-foreground); }
/* Settle: the needle arrives at the threshold rather than resting there
   from first paint — starts safely inside the caution zone and sweeps up
   to the critical line once, on load. .gauge-needle's own SVG coordinates
   already encode its resting angle; the keyframe's `to` is the identity
   transform, so the single-run animation (no `forwards`) hands off cleanly
   to that authored (unrotated) state with no jump. Pivots around the
   gauge's own centre (150,170 in the SVG's own coordinate space — the
   default transform-box for an SVG child is its viewport, so raw px here
   is already in that space, no fill-box override needed). The glow blooms
   in just as the needle finishes arriving, not before. */
.gauge-needle { transform-origin: 150px 170px; animation: needleSettle 1s var(--spring) .2s backwards; }
.gauge-glow { opacity: .25; transform-origin: 207.4px 88.1px; animation: gaugeGlowIn .5s var(--ease) 1s backwards; }
@keyframes needleSettle { from { transform: rotate(-75deg); } to { transform: rotate(0deg); } }
@keyframes gaugeGlowIn { from { opacity: 0; transform: scale(.4); } to { opacity: .25; transform: scale(1); } }
@media (prefers-reduced-motion: reduce) {
	.gauge-needle, .gauge-glow { animation: none; }
}
@media (max-width: 53.75em) { /* 860px */
	.industry-hero { padding: 8.4375rem 0 1.5rem; }
	.industry-hero-grid { grid-template-columns: 1fr; gap: 0.5rem; }
	.threshold-gauge { margin-top: 1.25rem; }
	.threshold-gauge svg { max-width: 17.5rem; }
}

/* Meet the Team (/company/about-us/) — real named leadership with the
   production site's own real photos. The photo is a small, centered
   portrait (roughly half the card width) rather than a card-filling
   image — it's an identifier, not the point of the card; the credentials
   below are. All bio text stays open (no "read more" toggle): every
   card expanding together on one click read as a bug, not a feature. */
#team { scroll-margin-top: 6rem; }
.team-grid { grid-template-columns: repeat(3, 1fr); gap: 1.75rem; }
/* Two-up variant. Left on the 3-col default, a pair of cards fills columns
   one and two and leaves a hole where a third used to be; this centres them
   at a width that keeps the bio measure readable rather than stretching each
   card to half the container. */
.team-grid--duo { grid-template-columns: repeat(2, 1fr); max-width: 51.25rem; margin-inline: auto; }
.team-card { text-align: center; }
.team-avatar {
	width: 62%; margin: 0 auto 1.25rem; aspect-ratio: 177 / 202; border-radius: 0.875rem;
	overflow: hidden; background: var(--muted);
}
.team-avatar img { width: 100%; height: 100%; object-fit: cover; display: block; }
.team-card-head { display: flex; align-items: center; justify-content: center; gap: 0.5rem; margin-bottom: 0.1875rem; }
.team-card h3 { font-size: 1.08rem; }
.team-linkedin {
	flex: none; display: flex; align-items: center; justify-content: center;
	width: 1.75rem; height: 1.75rem; border-radius: 50%;
	background: var(--accent); color: var(--teal-deep); transition: background .2s ease, color .2s ease, transform .2s ease;
}
.team-linkedin:hover { background: var(--teal-deep); color: #fff; transform: translateY(-2px); }
.team-linkedin svg { width: 0.875rem; height: 0.875rem; }
.team-role {
	display: block; font-family: var(--font-mono); font-size: var(--text-min); letter-spacing: .1em;
	text-transform: uppercase; color: var(--teal-deep); margin-bottom: 1rem;
}
.team-card p { text-align: left; font-size: .89rem; color: var(--muted-foreground); line-height: 1.62; }
.team-card p + p { margin-top: 0.75rem; }
@media (max-width: 53.75em) { /* 860px */ .team-grid { grid-template-columns: 1fr; max-width: 22.5rem; margin: 0 auto; } }
@media (max-width: 30em) { /* 480px */ .team-avatar { width: 68%; } }

/* Client testimonials — a step-by-one carousel (.carousel, shared markup
   with initCarousels() in main.js) rather than a static grid: seven real
   quotes read as a wall of text laid flat, one page at a time invites
   actually reading them. Initials-avatar attribution is still right here
   — these are named client contacts, not DivIni staff, so there's no
   source photo to reuse even if one were wanted. */
.carousel-viewport { overflow: hidden; }
.carousel-track { display: flex; gap: 1.5rem; transition: transform .5s var(--ease); will-change: transform; }
.carousel-slide { flex: 0 0 calc(50% - 0.75rem); min-width: 0; }
.testimonial-card { display: flex; flex-direction: column; height: 100%; }
.testimonial-card .quote-mark { color: var(--teal); opacity: .45; margin-bottom: 0.75rem; }
.testimonial-card p { font-size: .93rem; line-height: 1.68; color: var(--ink-70); flex: 1; }
.testimonial-foot { display: flex; align-items: center; gap: 0.75rem; margin-top: 1.25rem; padding-top: 1.125rem; border-top: 1px solid var(--hair-l); }
.testimonial-avatar {
	width: 2.5rem; height: 2.5rem; border-radius: 50%; flex: none;
	display: flex; align-items: center; justify-content: center;
	background: linear-gradient(135deg, var(--avatar-a, var(--teal)) 0%, var(--avatar-b, var(--beam)) 100%);
	color: #fff; font-family: var(--font-display); font-variation-settings: 'wdth' 106, 'wght' 720; font-size: .82rem;
}
.testimonial-name { font-weight: 600; font-size: .88rem; color: var(--ink); }
.testimonial-role { display: block; font-size: var(--text-min-sans); color: var(--muted-foreground); margin-top: 1px; }

.carousel-controls { display: flex; align-items: center; justify-content: center; gap: 1.375rem; margin-top: 2rem; }
.carousel-arrow {
	flex: none; width: 2.625rem; height: 2.625rem; border-radius: 50%; display: flex; align-items: center; justify-content: center;
	background: var(--card); border: 1px solid var(--border); color: var(--foreground);
	transition: border-color .2s ease, color .2s ease, transform .2s var(--spring);
}
.carousel-arrow:hover { border-color: var(--teal); color: var(--teal-deep); transform: translateY(-2px); }
.carousel-arrow svg { width: 1.125rem; height: 1.125rem; }
.carousel-dots { display: flex; align-items: center; gap: 0.5625rem; }
.carousel-dot { width: 0.5rem; height: 0.5rem; border-radius: 50%; background: var(--border); padding: 0; transition: background .2s ease, transform .2s ease; }
.carousel-dot.is-active { background: var(--teal-deep); transform: scale(1.35); }
@media (max-width: 53.75em) { /* 860px */ .carousel-slide { flex-basis: 100%; } }
@media (prefers-reduced-motion: reduce) {
	.carousel-track { transition: none; }
	.carousel-arrow:hover { transform: none; }
}

/* Industries we specialize in — reuses the industry hub's own cover-photo
   card treatment (.practice-cover.is-photo) so a visitor who's already
   seen /industries/ recognises the pattern instead of learning a new one. */
.industries-strip { grid-template-columns: repeat(3, 1fr); }
/* These are .card--practice cards, whose link is pushed to the foot with
   margin-top: auto. That resolves to 0 on the tallest card in a row, so its
   link sat flush under the paragraph, and on every card once the strip drops
   to two columns or one. The gap moves onto the paragraph as a floor, as in
   .ind-build below; the auto margin still absorbs any slack, so the links stay
   level across a row. Scoped to the strip: the cards on the services and
   industries pages carry sub-links and proof boxes that set their own gap. */
.industries-strip .card--practice > p { margin-bottom: 1.25rem; }

/* Two separate award cards, each with its own real badge graphic from
   divinisoft.com — SME 500 and Great Place To Work are two different
   certifications from two different bodies, so they get two cards rather
   than one combined image standing in for both. */
.award-showcase { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1.5rem; max-width: 40rem; margin: 0 auto; }
.award-showcase-card {
	display: flex; flex-direction: column; align-items: center; text-align: center; gap: 1.125rem;
	padding: 2.25rem 1.75rem; border-radius: var(--radius-lg); background: var(--card); border: 1px solid var(--border);
	transition: border-color .2s ease, transform .35s var(--ease);
}
.award-showcase-card:hover { border-color: var(--teal); transform: translateY(-0.25rem); }
.award-showcase-card img { max-width: 9.25rem; height: auto; }
.award-showcase-card h3 { font-size: 1.05rem; }
.award-showcase-card span { font-size: .85rem; color: var(--muted-foreground); }
@media (max-width: 40em) { /* 640px */ .award-showcase { grid-template-columns: 1fr; max-width: 20rem; } }
@media (prefers-reduced-motion: reduce) { .award-showcase-card:hover { transform: none; } }

/* Bigger on the homepage: the about-us sizing was tuned to sit among other
   copy on that page, but here Recognition is the whole section — just two
   certifications now, not four, so they get more room instead of floating
   small in a wide section. */
.award-showcase--home { gap: 2rem; max-width: 53.75rem; }
.award-showcase--home .award-showcase-card { padding: 3rem 2.5rem; gap: 1.5rem; }
.award-showcase--home .award-showcase-card img { max-width: 11.875rem; }
.award-showcase--home .award-showcase-card h3 { font-size: 1.3rem; }
.award-showcase--home .award-showcase-card span { font-size: .92rem; }
@media (max-width: 40em) { /* 640px */ .award-showcase--home { max-width: 20rem; } }

/* Field shot — a second photograph paired with copy, further down the
   page. Mirrors .split's proportions but swappable in direction per page
   via .reverse, so three pages in a row don't all lead with the image on
   the same side. */
.field-shot { display: grid; grid-template-columns: .84fr 1.16fr; gap: 3rem; align-items: center; }
.field-shot.reverse { grid-template-columns: 1.16fr .84fr; }
.field-shot.reverse .photo-duo { order: 2; }
.field-shot .photo-duo { aspect-ratio: 5 / 4; }
.field-shot-copy .eyebrow { margin-bottom: 1rem; }
.field-shot-copy h2 { font-size: clamp(1.7rem, 2.6vw, 2.15rem); margin-bottom: 1rem; }
.field-shot-copy p { color: var(--muted-foreground); font-size: 1.02rem; line-height: 1.65; }
.field-shot-copy p + p { margin-top: 0.875rem; }

@media (max-width: 61.25em) { /* 980px */
	.photo-hero .split { grid-template-columns: 1fr; }
	.photo-hero .hero-stats { grid-template-columns: repeat(2, auto); row-gap: 1.5rem; }
	.field-shot, .field-shot.reverse { grid-template-columns: 1fr; }
	/* order: 1 (not a removed rule) because .field-shot's own DOM order is
	   photo-then-copy -- unlike .hero-copy/.hero-visual, simply dropping the
	   override wouldn't put copy first here, since half these sections have
	   no override at all to drop. Was order: -1 (photo leads); flipped for
	   the same reason the hero itself did (see the Responsive section's
	   .hero-visual comment): copy is the claim, the photo supports it, and
	   with the hero itself now headline-first, a .field-shot straight after
	   it that still opened with a second, similar-toned stock photo read as
	   two images back to back with no text between them. order: 1 (not 0)
	   so it's unambiguously after copy's default 0 regardless of which of
	   the two DOM orders (.field-shot vs .field-shot.reverse) a given
	   section starts from. */
	.field-shot .photo-duo, .field-shot.reverse .photo-duo { order: 1; aspect-ratio: 16 / 10; }
}
@media (max-width: 40em) { /* 640px */
	.photo-hero { padding: 8.25rem 0 4rem; }
	.photo-duo { aspect-ratio: 16 / 11; }
}

/* ==========================================================================
   Industry family (docs/design/industry-family-design.md) — quant-finance,
   payments and health-insurance, scoped via .ind-hero and .ind-build
   only. Nothing here edits .photo-hero, .svc-proof,
   .card or .svc-phases directly, so /company/about-us/, /company/careers/
   and every other page sharing those classes (.svc-proof alone spans 14
   files) render exactly as before -- true for every property except mobile
   hero source order, which these three pages got first (copy before photo,
   reasoning below) and the rest of the site has since adopted as its own
   default; see the .hero-visual comment in the Responsive section. Health
   Insurance is the reference implementation this section was built against;
   quant-finance and payments adopt the same .ind-* classes rather than
   getting their own copies — these three pages are one family and read as
   siblings by design (see the doc), not three independently-styled pages.
   ========================================================================== */

/* What we build — a mono kicker per card, each word lifted straight from
   that card's own paragraph (Agent, Self-serve, Audit trail, the two named
   platforms, Full-stack, Run log) rather than invented, so the grid reads
   as six specific capabilities instead of one card shape repeated six
   times. */
.ind-build .card-kicker {
	display: block; font-family: var(--font-mono); font-size: var(--text-min); font-weight: 600;
	letter-spacing: .12em; text-transform: uppercase; color: var(--teal-deep); margin-bottom: 0.875rem;
}
/* Grid rows stretch every card to the same height, but a block card leaves
   its link wherever its paragraph ends, so links sat a line apart across a
   row. Same fix as .card--practice: flex column, link pushed to the foot.
   The 16px gap .card-link used to carry moves to the paragraph, so the
   tallest card in a row keeps it once margin-top: auto resolves to 0. */
.ind-build .card { display: flex; flex-direction: column; }
.ind-build .card > p { margin-bottom: 1rem; }
.ind-build .card-link { margin-top: auto; align-self: start; }

/* Quant Finance splits its cards into two named rows: two it develops or
   integrates, three ways it validates. The label is darker than the cards'
   teal kickers and runs into a hairline, so it reads as the row's name,
   not as one more card's kicker. */
.ind-build-group + .ind-build-group { margin-top: 2.75rem; }
.ind-build-label {
	display: flex; align-items: center; gap: 1rem; margin: 0 0 1.125rem;
	font-family: var(--font-mono); font-size: .8rem; font-weight: 600;
	letter-spacing: .14em; text-transform: uppercase; color: var(--foreground);
}
.ind-build-label::after { content: ''; flex: 1; height: 1px; background: var(--border); }
/* .grid-3 drops to two columns below 980px; the third card then spans the
   row instead of sitting alone beside an empty cell. Below 640px every
   card is one column anyway. */
@media (min-width: 40.0625em) and (max-width: 61.25em) { /* 641-980px */
	.ind-build-group .grid-3 > .card:last-child:nth-child(odd) { grid-column: 1 / -1; }
}

/* What we offer (Quant Finance). Three engagement shapes you choose
   between, so they sit side by side on one rule like a spec sheet, not as
   three more cards. Subgrid lines up heading, description, "Best when"
   and link across the columns whatever each one's copy length; the flex
   column is the fallback where subgrid isn't supported. */
.ind-offer-grid { display: grid; grid-template-columns: repeat(3, 1fr); border-top: 1px solid var(--hair-l); }
.ind-offer-item { display: flex; flex-direction: column; padding: 2.125rem 2.125rem 0.25rem; }
.ind-offer-item + .ind-offer-item { border-left: 1px solid var(--hair-l); }
.ind-offer-item:first-child { padding-left: 0; }
.ind-offer-item:last-child { padding-right: 0; }
.ind-offer-item h3 { font-size: 1.3rem; margin-bottom: 0.75rem; }
.ind-offer-desc { line-height: 1.65; color: var(--ink-70); }
.ind-offer-fit { margin-top: auto; padding-top: 1.375rem; font-size: .93rem; line-height: 1.55; color: var(--ink); }
.ind-offer-fit b { font-weight: 600; color: var(--teal-ink); }
.ind-offer-item .card-link { margin-top: 0.875rem; align-self: start; justify-self: start; }
@supports (grid-template-rows: subgrid) {
	.ind-offer-item { display: grid; grid-row: span 4; grid-template-rows: subgrid; row-gap: 0; }
	.ind-offer-fit { margin-top: 0; }
}

@media (max-width: 61.25em) { /* 980px */
	.ind-offer-grid { grid-template-columns: 1fr; }
	.ind-offer-item, .ind-offer-item:first-child, .ind-offer-item:last-child { display: flex; grid-row: auto; padding: 1.75rem 0 1.625rem; }
	.ind-offer-item + .ind-offer-item { border-left: 0; border-top: 1px solid var(--hair-l); }
	.ind-offer-fit { padding-top: 1rem; }
}

/* ==========================================================================
   Hiring journey — a vertical, alternating timeline rather than the flat
   3-up numbered rail .svc-phases uses elsewhere. Lives on /open-jobs/,
   right where someone who's found a role is deciding whether to click
   Apply — not on the main careers page, which is about the pitch, not the
   mechanics. Reuses the shared .reveal system (see initReveal in main.js)
   for the section's entrance and each step's stagger; only the connecting
   line's fill is bespoke, triggered off the same .is-visible class.
   ========================================================================== */
.journey { position: relative; max-width: 53.75rem; margin: 0 auto; padding: 0.25rem 0 0; }
.journey-line {
	position: absolute; left: 50%; top: 0.625rem; bottom: 0.625rem; width: 2px;
	background: var(--border); transform: translateX(-1px); z-index: 0; overflow: hidden;
}
.journey-line-fill {
	position: absolute; left: 0; top: 0; width: 100%; height: 0;
	background: linear-gradient(180deg, var(--teal) 0%, var(--sun) 100%);
	transition: height 1.7s var(--ease) .15s;
}
.journey.is-visible .journey-line-fill { height: 100%; }

.journey-step {
	position: relative; display: grid; grid-template-columns: 1fr 4.25rem 1fr;
	align-items: center; column-gap: 2.25rem; margin-bottom: 3.75rem;
}
.journey-step:last-child { margin-bottom: 0; }
.journey-node {
	grid-column: 2; position: relative; z-index: 1; justify-self: center;
	width: 4.25rem; height: 4.25rem; border-radius: 50%;
	background: var(--card); border: 2px solid var(--border); color: var(--muted-foreground);
	display: flex; align-items: center; justify-content: center;
	transition: background .5s var(--ease), border-color .5s var(--ease), color .5s var(--ease), transform .5s var(--spring);
}
.journey-step.is-visible .journey-node {
	background: var(--teal-deep); border-color: var(--teal-deep); color: #fff; transform: scale(1.08);
}
.journey-node svg { width: 1.6875rem; height: 1.6875rem; }
.journey-node-num {
	position: absolute; top: -0.5rem; right: -0.5rem; width: 1.5rem; height: 1.5rem; border-radius: 50%;
	background: var(--sun); color: #fff; font-family: var(--font-mono); font-size: var(--text-min); font-weight: 700;
	display: flex; align-items: center; justify-content: center; box-shadow: 0 0 0 3px var(--background);
}
.journey-card {
	grid-column: 1; padding: 1.5rem 1.75rem; border-radius: var(--radius);
	background: var(--card); border: 1px solid var(--border);
}
.journey-step--right .journey-card { grid-column: 3; }
.journey-when {
	display: block; font-family: var(--font-mono); font-size: var(--text-min); font-weight: 500; letter-spacing: .12em;
	text-transform: uppercase; color: var(--teal-deep); margin-bottom: 0.5625rem;
}
.journey-card h3 { font-size: 1.1rem; margin-bottom: 0.5625rem; }
.journey-card p { font-size: .93rem; color: var(--muted-foreground); line-height: 1.58; }

@media (max-width: 45em) { /* 720px */
	.journey-line { left: 1.8125rem; }
	.journey-step, .journey-step--right { grid-template-columns: 3.75rem 1fr; column-gap: 1.25rem; }
	.journey-node, .journey-step--right .journey-node { grid-column: 1; justify-self: start; width: 3.75rem; height: 3.75rem; }
	.journey-node svg { width: 1.4375rem; height: 1.4375rem; }
	.journey-card, .journey-step--right .journey-card { grid-column: 2; }
}
@media (prefers-reduced-motion: reduce) {
	.journey-line-fill { height: 100%; transition: none; }
	.journey-node { transition: background .2s, border-color .2s, color .2s; }
	.journey-step.is-visible .journey-node { transform: none; }
}

/* ==========================================================================
   Open-jobs listing (/company/careers/open-jobs/) — a practice/location
   filter bar over the same .role-list/.role-card component the main
   careers page teaser uses, so a role looks identical whether you meet it
   there or here. Filtering is pure client-side (initJobFilters in main.js)
   since the whole site is static.
   ========================================================================== */
.job-filters { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 0.75rem 1rem; margin: 0 0 2.5rem; }
.job-filter-group { display: flex; flex-wrap: wrap; justify-content: center; gap: 0.5rem; }
.job-filter-pill {
	padding: 0.5625rem 1.125rem; border-radius: 999px; background: var(--card); border: 1px solid var(--border);
	font-size: .84rem; font-weight: 600; color: var(--ink-70);
	transition: border-color .2s ease, color .2s ease, background .2s ease;
}
.job-filter-pill:hover { border-color: var(--teal); color: var(--teal-deep); }
.job-filter-pill.is-active { background: var(--teal-deep); border-color: var(--teal-deep); color: #fff; }
.job-filter-select-wrap { position: relative; }
.job-filter-select {
	padding: 0.5625rem 2.375rem 0.5625rem 1.125rem; border-radius: 999px; background: var(--card); border: 1px solid var(--border);
	font-size: .84rem; font-weight: 600; color: var(--ink-70); font-family: inherit;
	appearance: none; -webkit-appearance: none; cursor: pointer;
}
.job-filter-select:hover { border-color: var(--teal); }
.job-filter-select-wrap::after {
	content: ''; position: absolute; right: 1rem; top: 50%; width: 0.5rem; height: 0.5rem;
	border-right: 1.5px solid var(--ink-70); border-bottom: 1.5px solid var(--ink-70);
	transform: translateY(-65%) rotate(45deg); pointer-events: none;
}
.role-list-empty {
	text-align: center; padding: 3.5rem 1.5rem; color: var(--muted-foreground);
	border: 1px dashed var(--border); border-radius: var(--radius);
}
.role-list-empty[hidden] { display: none; }

/* ==========================================================================
   Generative AI service page — the light counterpart

   The industry pages and Agentic AI open on ink; this one deliberately does
   not. Generative AI's argument is provenance — an answer is only worth the
   sources under it — and provenance is a reading problem, so the page is
   built like a page of paper: daylight ground, faint horizontal rules
   behind the hero, and cobalt/teal/persimmon used as *citation* colours
   rather than decoration. Cobalt leads because that is Gen AI's identity
   colour in the services hub cover and the mega-menu rail.
   ========================================================================== */
/* Revised from an earlier all-paper hero: kept for the rest of the page
   (the capability rows below are still daylight — provenance is a reading
   problem, paper still suits it), but the hero itself moved to ink so it
   opens with more weight. Same dark-token trick as .photo-hero, redeclared
   here rather than shared so this file's colour story stays self-contained
   and doesn't drift if that class changes for the industry pages later. */
.genai-hero {
	--background: var(--hero-ink); --foreground: #EAF3F1; --surface: var(--hero-ink);
	--card: var(--hero-ink-raise); --muted-foreground: var(--lite-72);
	--accent: rgba(43,52,224,.16); --border: rgba(234,243,241,.14);
	background: var(--background); color: var(--foreground);
	position: relative; overflow: hidden;
	padding: 9.5rem 0 5.25rem;
}
/* Same aurora wash the homepage and industry heroes use, reused rather than
   reinvented, with its own drift period so three auroras on three different
   pages don't breathe in lockstep if a visitor has more than one open. */
.genai-hero::before {
	content: ''; position: absolute; inset: -20% -10% auto -10%; height: 40rem; pointer-events: none;
	background: var(--gradient-aurora); opacity: .5; filter: blur(48px);
	background-size: 160% 160%; animation: aurora-drift 22s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) { .genai-hero::before { animation: none; } }

/* Live particle-network hero background — replaces the static aurora blob
   above with a canvas (see the inline script at the bottom of this page)
   drawing a drifting node network plus the same breathing aurora glow,
   scoped to .genai-hero itself. Trialled first on /services/
   generative-ai-design-test/ — kept as position:absolute inside .genai-hero
   (already position:relative) rather than position:fixed to the viewport,
   since the sections below the hero don't paint their own opaque
   background and a viewport-fixed canvas leaked through them. */
.genai-hero.is-live-network::before { display: none; }
#genai-network-bg { position: absolute; inset: 0; z-index: 0; pointer-events: none; display: block; }
.genai-hero .container { position: relative; z-index: 1; }
.genai-hero .split { display: grid; grid-template-columns: 1.02fr .98fr; gap: 3.5rem; align-items: center; }
.genai-hero .hero-visual { top: 0; }
.genai-hero .breadcrumb a:hover { color: #fff; }
.genai-hero .eyebrow { color: var(--teal); }
.genai-hero h1 { font-size: clamp(2.25rem, 4.3vw, 3.4rem); max-width: 15ch; }
.genai-hero h1 em {
	font-style: normal; color: var(--teal);
	/* Cobalt was the light hero's highlight, but it's 2.3:1 on ink — under
	   the floor even at headline size (3:1). Teal holds 5.4:1 on ink and
	   stays in the same Gen-AI cobalt/teal pairing the services-hub cover
	   and mega-menu rail already use, so the family doesn't change, only
	   which half of it carries the text. */
}
.genai-lede { margin-top: 1.375rem; max-width: 52ch; font-size: 1.12rem; line-height: 1.62; color: var(--lite-72); }
.genai-hero .hero-cta { margin-top: 2rem; }

/* Light photo treatment — a multiply wash rather than the industry pages'
   grayscale duotone. On an already-bright photograph multiply tints the
   shadows toward brand without darkening the whole frame, which is what
   keeps a light page light. */
.photo-light {
	position: relative; border-radius: var(--radius-lg); overflow: hidden;
	border: 1px solid var(--hair-l); box-shadow: var(--shadow-card); background: #fff;
}
.photo-light img { display: block; width: 100%; height: 100%; object-fit: cover; }
.photo-light::after {
	content: ''; position: absolute; inset: 0; pointer-events: none; mix-blend-mode: multiply;
	background: linear-gradient(148deg, rgba(43,52,224,.13) 0%, rgba(93,160,172,.11) 55%, rgba(255,106,69,.07) 100%);
}
.genai-hero .photo-duo { aspect-ratio: 4 / 3; }

/* --- signature: the provenance card ---------------------------------------
   The page's one bold element, and the thing it should be remembered by.
   Every other generative-AI pitch shows a chat bubble; the argument here is
   the opposite of a chat bubble — that a generated sentence is only worth
   the passage underneath it. So the answer is shown *with its citations
   still attached*: each cited clause carries a numbered marker, and the
   retrieved passage that produced it sits below in the matching colour.
   Colour is doing indexing work here, so it is never decorative — three
   sources, three hues, and nothing else on the card is coloured at all. */
.provenance {
	background: var(--paper-hi); border: 1px solid var(--hair-l);
	border-radius: var(--radius-lg); box-shadow: var(--shadow-card);
	/* All three citation hues have to carry white numerals at 10.5px and read
	   as text on paper, so the warm one is --sun-deep, not --sun (2.8:1). */
	padding: 2.125rem; --c1: var(--beam); --c2: var(--teal-deep); --c3: var(--sun-deep);
}
.prov-query {
	display: flex; align-items: flex-start; gap: 0.625rem; padding-bottom: 1.25rem;
	border-bottom: 1px solid var(--hair-l); font-family: var(--font-mono);
	font-size: 0.78125rem; line-height: 1.5; color: var(--ink-70);
}
.prov-query svg { flex: none; width: 0.9375rem; height: 0.9375rem; margin-top: 1px; color: var(--beam); }
.prov-answer {
	margin: 1.375rem 0 1.625rem; font-size: 1.12rem; line-height: 1.75; color: var(--ink);
	max-width: 68ch;
}
/* The cited clause keeps a coloured baseline rule rather than a highlighter
   fill — a fill would make the answer look redacted, and the point is that
   the sentence is trustworthy, not that it is flagged. */
.prov-claim { border-bottom: 2px solid var(--claim, var(--beam)); padding-bottom: 1px; }
.prov-cite {
	display: inline-flex; align-items: center; justify-content: center;
	min-width: 1.0625rem; height: 1.0625rem; margin-left: 0.1875rem; padding: 0 0.25rem;
	border-radius: 0.3125rem; background: var(--claim, var(--beam)); color: #fff;
	font-family: var(--font-mono); font-size: var(--text-min); font-weight: 500;
	vertical-align: super; line-height: 1;
}
.prov-sources { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; }
.prov-source {
	position: relative; padding: 1rem 1rem 0.9375rem; border-radius: 0.875rem;
	background: var(--popover); border: 1px solid var(--hair-l);
	border-top: 3px solid var(--claim, var(--beam));
	transition: transform .3s var(--ease), box-shadow .3s var(--ease);
}
.prov-source:hover { transform: translateY(-0.1875rem); box-shadow: var(--shadow-soft); }
.prov-source-head {
	display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5625rem;
	font-family: var(--font-mono); font-size: var(--text-min); color: var(--ink);
	overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.prov-source-n {
	flex: none; display: inline-flex; align-items: center; justify-content: center;
	width: 1.0625rem; height: 1.0625rem; border-radius: 0.3125rem;
	background: var(--claim, var(--beam)); color: #fff; font-size: var(--text-min); font-weight: 500;
}
.prov-source p { font-size: .84rem; line-height: 1.55; color: var(--ink-70); }
.prov-foot {
	margin-top: 1.375rem; padding-top: 1.0625rem; border-top: 1px solid var(--hair-l);
	display: flex; align-items: center; gap: 0.5625rem;
	font-family: var(--font-mono); font-size: var(--text-min); color: var(--ink-70);
}
.prov-foot svg { flex: none; width: 0.9375rem; height: 0.9375rem; color: var(--teal-deep); }

/* --- light diagram box for the deep-dive column -------------------------- */
.genai-diagram {
	margin: 1.625rem 0 0; padding: 1.375rem; border-radius: var(--radius);
	background: var(--paper-hi); border: 1px solid var(--hair-l);
}
.genai-diagram-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }
.genai-diagram-scroll svg { display: block; width: 100%; height: auto; }
.genai-diagram figcaption {
	margin-top: 0.875rem; font-family: var(--font-mono); font-size: var(--text-min);
	line-height: 1.55; color: var(--ink-45); max-width: 64ch;
}
.genai-diagram figcaption { padding-top: 0.8125rem; border-top: 1px solid var(--hair-l); }

@media (max-width: 61.25em) { /* 980px */
	.genai-hero .split { grid-template-columns: 1fr; gap: 2.5rem; }
	.prov-sources { grid-template-columns: 1fr; }
	.provenance { padding: 1.625rem 1.375rem; }
}
@media (max-width: 45em) { /* 720px */
	/* Diagrams keep their drawn size and scroll in their own track below the
	   width where their 10-12px labels would scale under ~9px. The floor is
	   a vw-derived max-width on the wrapper, never a min-width on the SVG —
	   a min-width there feeds back into the column's min-content size and
	   drags the body text off-screen with it. */
	.genai-diagram-scroll { max-width: calc(100vw - 5.75rem); }
	.genai-diagram-scroll svg { min-width: 31.25rem; }
	/* Same header-clearance fix as the rest of this hero family (see
	   .services-hero's 860px block) -- this one's own reduced-padding
	   breakpoint is 720px rather than 860px, but the fixed header it needs
	   to clear is identical. */
	.genai-hero { padding: 8.4375rem 0 4.25rem; }
	.genai-hero .photo-duo { aspect-ratio: 16 / 11; }
}

/* ==========================================================================
   Generative AI — capability showcase (rail-free variant)

   The sidebar rail assumes an article you read top to bottom in order.
   These four practices aren't ordered — a visitor wants whichever one maps
   to what they're building, so the replacement is a set of jump-to pills
   (reusing /services/'s own practice-map component) plus four full-width
   rows a visitor can scan without a fixed sidebar competing for width on
   a page that's already tight at 720px. Each row's visual is native to
   what that practice actually produces — a data transform, a reviewed
   image batch, a conversation, a pipeline — rather than one diagram
   template reskinned four times.
   ========================================================================== */
.cap-lead {
	display: grid; grid-template-columns: .8fr 1.2fr; gap: 2.5rem; align-items: center;
	margin: 0.5rem 0 3.5rem; padding: 1.625rem; border-radius: var(--radius-lg);
	background: var(--paper-hi); border: 1px solid var(--hair-l);
}
.cap-lead .photo-light { aspect-ratio: 16 / 10; }
.cap-lead p { font-size: 1.02rem; line-height: 1.65; color: var(--ink-70); }
.cap-lead p b { color: var(--ink); font-weight: 600; }

.capabilities { display: flex; flex-direction: column; gap: 5.5rem; }
/* Explicit order on BOTH children, not just a lone override on .reverse's
   media — a previous version set order:2 on .reverse's media alone, which
   sits above copy's *implicit* order:0 either way and never actually swaps
   which side either one renders on, only which grid track is wider. Every
   row now states which side it's on; .reverse flips both, so left-image
   and left-text rows genuinely alternate instead of only resizing. */
.capability { display: grid; grid-template-columns: 1.08fr .92fr; gap: 3.25rem; align-items: start; scroll-margin-top: 7.25rem; }
.capability-media { order: 1; }
.capability-copy { order: 2; }
.capability.reverse { grid-template-columns: .92fr 1.08fr; }
.capability.reverse .capability-media { order: 2; }
.capability.reverse .capability-copy { order: 1; }
.capability-tag {
	display: inline-flex; align-items: center; gap: 0.5rem; margin-bottom: 1rem;
	font-family: var(--font-mono); font-size: var(--text-min); font-weight: 500;
	letter-spacing: .12em; text-transform: uppercase; color: var(--cap-accent, var(--beam));
}
.capability-tag::before { content: ''; width: 0.5rem; height: 0.5rem; border-radius: 50%; background: var(--cap-accent, var(--beam)); }
.capability h3 { font-size: clamp(1.4rem, 2.2vw, 1.7rem); margin-bottom: 0.875rem; }
.capability > .capability-copy > p { font-size: 1.02rem; line-height: 1.65; color: var(--ink-70); margin-bottom: 1.25rem; }
.capability .outcome-list { margin-bottom: 0; }
.capability-media { position: relative; display: flex; flex-direction: column; gap: 0.875rem; }
/* Soft ambient wash behind each row's visual, tinted per-row from the same
   --cap-accent the tag/dot already use — the "gradient in the background"
   restrained to one blurred blob at low opacity rather than a full-bleed
   panel, so it reads as light falling on the visual, not a coloured card. */
.capability-media::before {
	content: ''; position: absolute; z-index: -1; pointer-events: none;
	inset: -12% -16%; border-radius: 40%;
	background: radial-gradient(60% 60% at 50% 42%, var(--cap-accent, var(--beam)), transparent 72%);
	opacity: .16; filter: blur(38px);
}
.capability-media-caption { font-family: var(--font-mono); font-size: var(--text-min); line-height: 1.55; color: var(--ink-45); }

/* --- capability-media background variants (design test) -----------------
   The base ::before above is one blurred blob recoloured per practice — it
   never actually varies except in hue. These five give each practice its
   own pattern instead, tied to what that practice is actually about: a
   schema grid for structured output, a colour mesh for image generation,
   ripples for a live conversation, the hero's own paper rule for
   retrieval/documents, and route-lines converging on a point for the MCP
   server's hub-and-spoke shape. Fine line patterns replace the blur with a
   radial mask fade (matching .genai-hero::before's ruled-line technique) —
   a 38px blur would turn a 1px grid line to mush. Kept to the same low
   opacity band (.10–.16) as the base rule so none of the five reads as
   louder than the others. */
.capability-media--grid::before {
	background: none;
	background-image:
		repeating-linear-gradient(to right, var(--cap-accent, var(--beam)) 0 1px, transparent 1px 2.125rem),
		repeating-linear-gradient(to bottom, var(--cap-accent, var(--beam)) 0 1px, transparent 1px 2.125rem);
	opacity: .1; filter: none;
	-webkit-mask-image: radial-gradient(70% 70% at 50% 46%, #000 30%, transparent 78%);
	        mask-image: radial-gradient(70% 70% at 50% 46%, #000 30%, transparent 78%);
}
.capability-media--mesh::before {
	background:
		radial-gradient(46% 60% at 18% 20%, var(--beam), transparent 68%),
		radial-gradient(50% 64% at 82% 30%, var(--teal-ink), transparent 66%),
		radial-gradient(44% 56% at 54% 92%, var(--sun-deep), transparent 66%);
	opacity: .16; filter: blur(34px);
}
.capability-media--ripple::before {
	background: repeating-radial-gradient(circle at 50% 42%, var(--cap-accent, var(--sun-deep)) 0 1.5px, transparent 1.5px 1.625rem);
	opacity: .14; filter: none;
	-webkit-mask-image: radial-gradient(65% 65% at 50% 42%, #000 20%, transparent 76%);
	        mask-image: radial-gradient(65% 65% at 50% 42%, #000 20%, transparent 76%);
}
.capability-media--rule::before {
	background: none;
	background-image: repeating-linear-gradient(to bottom, var(--cap-accent, var(--beam)) 0 1px, transparent 1px 0.9375rem);
	opacity: .1; filter: none;
	-webkit-mask-image: radial-gradient(80% 70% at 46% 44%, #000 26%, transparent 80%);
	        mask-image: radial-gradient(80% 70% at 46% 44%, #000 26%, transparent 80%);
}
.capability-media--route::before {
	background: conic-gradient(from 200deg at 8% 30%,
		transparent 0deg, var(--cap-accent, var(--teal-ink)) 2deg, transparent 5deg,
		transparent 24deg, var(--cap-accent, var(--teal-ink)) 26deg, transparent 29deg,
		transparent 48deg, var(--cap-accent, var(--teal-ink)) 50deg, transparent 53deg,
		transparent 72deg, var(--cap-accent, var(--teal-ink)) 74deg, transparent 77deg,
		transparent 96deg);
	opacity: .12; filter: none;
	-webkit-mask-image: radial-gradient(75% 75% at 8% 30%, #000 10%, transparent 74%);
	        mask-image: radial-gradient(75% 75% at 8% 30%, #000 10%, transparent 74%);
}

/* --- reference strip (Image & Video Generation) --------------------------- */
.ref-strip { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.625rem; }
.ref-strip .photo-light { aspect-ratio: 1; border-radius: 0.75rem; }
.ref-strip .photo-light::after { opacity: .55; }

/* --- chat transcript mock (AI Chatbots) ----------------------------------- */
.chat-mock {
	background: var(--paper-hi); border: 1px solid var(--hair-l); border-radius: var(--radius);
	box-shadow: var(--shadow-card); padding: 1.25rem; display: flex; flex-direction: column; gap: 0.75rem;
}
.chat-msg { display: flex; flex-direction: column; gap: 0.3125rem; max-width: 84%; }
.chat-msg--user { align-self: flex-end; align-items: flex-end; }
.chat-msg--bot { align-self: flex-start; align-items: flex-start; }
.chat-bubble { padding: 0.625rem 0.875rem; border-radius: 0.875rem; font-size: .88rem; line-height: 1.5; }
.chat-msg--user .chat-bubble { background: var(--accent); color: var(--ink); border-bottom-right-radius: 0.25rem; }
.chat-msg--bot .chat-bubble { background: var(--popover); border: 1px solid var(--hair-l); color: var(--ink); border-bottom-left-radius: 0.25rem; }
.chat-cite {
	display: inline-flex; align-items: center; gap: 0.3125rem; margin-top: 0.4375rem;
	font-family: var(--font-mono); font-size: var(--text-min); color: var(--teal-deep);
}
.chat-msg--escalate { align-self: center; max-width: 100%; width: 100%; }
.chat-msg--escalate .chat-bubble {
	width: 100%; text-align: center; background: transparent; border: 1px dashed var(--hair-l);
	font-family: var(--font-mono); font-size: var(--text-min); color: var(--ink-45);
}
.chat-msg--escalate b { color: var(--sun-deep); font-weight: 600; }
.chat-foot {
	margin-top: 0.25rem; padding-top: 0.75rem; border-top: 1px solid var(--hair-l);
	font-family: var(--font-mono); font-size: var(--text-min); color: var(--ink-45);
}

@media (max-width: 61.25em) { /* 980px */
	.cap-lead { grid-template-columns: 1fr; }
	.capability, .capability.reverse { grid-template-columns: 1fr; gap: 0; }
	/* heading -> paragraph -> visual -> bullets. .capability-copy carries no
	   layout of its own -- no display/flex/gap, its children already space
	   themselves via their own margins on desktop -- so display: contents
	   here is lossless: tag/h3/p/list become direct grid items of
	   .capability itself instead of one fused block, which is what actually
	   lets the visual land between the paragraph and the bullet list. An
	   order swap on .capability-copy as a whole (the previous two mobile
	   treatments, media-first and copy-first) could only ever move media
	   before or after the *entire* copy block, never inside it. Grid gap
	   drops to 0 here because five real items now share the one column
	   instead of two -- spacing is each item's own margin-bottom below,
	   keeping the tight tag/h3/paragraph rhythm .capability-copy already
	   had on desktop, with a wider margin only where the content actually
	   changes register (paragraph to visual, visual to the bullet list). */
	.capability > .capability-copy { display: contents; }
	.capability-tag, .capability h3 { order: 1; }
	.capability > .capability-copy > p { order: 2; margin-bottom: 1.75rem; }
	/* .capability.reverse .capability-media's own order: 2 (desktop, above)
	   is more specific than a bare .capability-media rule and would still
	   win here if not named explicitly. */
	.capability .capability-media, .capability.reverse .capability-media { order: 3; margin-bottom: 1.75rem; }
	.capability .outcome-list { order: 4; }
	.capabilities { gap: 4rem; }
}
@media (max-width: 35em) { /* 560px */
	.ref-strip { grid-template-columns: repeat(3, 1fr); gap: 0.375rem; }
	.chat-msg { max-width: 92%; }
}

/* ==========================================================================
   Industry family hero — pin-and-shrink (docs/design/industry-family-design.md).
   Shared by all three industry pages (quant-finance, payments,
   health-insurance). Built and tuned first on
   Health Insurance alone (page-scoped as .hi-hero-stage, --hi- custom
   properties, #hiHeroStage), then generalised here once Payments and
   Quant Finance needed the exact
   same mechanism — same promotion this family already went through once for
   .ind-build.

   Reproduces the homepage's own #heroStage/.hero technique (--shrink-x/
   --pe and the .hero-stage/.hero-stage__sticky/.hero-stage-grid/.hero
   rules near the top of this file) under family-scoped selectors and
   custom-property names, instead of reusing the homepage's literal
   .hero-stage/.hero classes or #heroStage/#hero ids. Two real reasons:

   1. Those ids are load-bearing. initHeroStage() in main.js looks
      #heroStage/#hero up directly, and initHeroHeader() explicitly treats
      "#heroStage present anywhere on the page" as "this is the homepage,
      defer to initHeroStage instead." Reusing them here would silently
      repoint both functions at every page below in ways neither was
      written or tested against. #indHeroStage is reused identically
      (literally the same id) across all three pages' own markup — safe,
      since each is a separate HTML document; no cross-page collision.
   2. Wrapping .ind-hero in a stage takes it out of being #primary's direct
      child, which is exactly what #primary:has(> :is(...photo-hero...))
      and .site-header:has(~ #primary > :is(...photo-hero...)) above key
      off for header-clearance padding and the transparent-until-scrolled
      header. Rather than edit those shared selectors — which the other
      nine heroes in that :is() list still need to match precisely as they
      do today — the additive rules below duplicate the same declarations
      under an .ind-hero-stage-keyed selector instead.

   Numbers started as the homepage's own already-tuned values (--shrink-x
   .06/.028, 52px corner radius), then were retuned once on Health
   Insurance (shrink halved, radius cut to roughly a third) and carried
   forward here unchanged for the other two pages — the same shrink feel
   across all three is the point, matching how the family already shares
   one canvas contract and one --ind-* component set with only the content
   differing per page. The ease-in-out curve, the 113vh/109vh stage-height
   proportions and the reduced-motion fallback shape are reused verbatim
   from the homepage throughout. */
.ind-hero-stage {
	--ind-shrink-x: .03;
	--ind-radius: 1.08125rem;
	--ind-pe: 0;
	position: relative;
	height: 113vh;
}
.ind-hero-stage__sticky {
	position: sticky; top: 0; height: 100vh; height: 100dvh;
	display: grid; place-items: center; overflow: clip;
}
/* Same "fixed backdrop, uncovered at the shrinking card's margins" role as
   .hero-stage-grid, in this family's own light-on-dark hairline tone
   instead of the homepage's paper-toned one — .ind-hero-stage sits on
   --hero-ink via .ind-hero, not --paper. */
.ind-hero-stage-grid {
	position: absolute; inset: 0; opacity: .5; pointer-events: none;
	background-image:
		linear-gradient(rgba(234,243,241,.06) 1px, transparent 1px),
		linear-gradient(90deg, rgba(234,243,241,.06) 1px, transparent 1px);
	background-size: 2.5rem 2.5rem;
	-webkit-mask-image: radial-gradient(120% 90% at 50% 50%, #000 30%, transparent 82%);
	        mask-image: radial-gradient(120% 90% at 50% 50%, #000 30%, transparent 82%);
}
/* .photo-hero/.ind-hero already set position:relative and overflow:hidden
   (see their own base rules) — only height and the shrink itself are new
   here. Height was auto (content-driven, ~1000-1035px at 1440x900 across
   the three pages, measured) before this; capping it at 100dvh is what
   makes the sticky pin-and-hold read cleanly, the same reason the
   homepage's own .hero is 100dvh rather than content-height. */
.ind-hero-stage .ind-hero {
	width: 100%; height: 100vh; height: 100dvh;
	clip-path: inset(0 calc(var(--ind-shrink-x) * var(--ind-pe) * 50%) 0 calc(var(--ind-shrink-x) * var(--ind-pe) * 50%) round calc(var(--ind-radius) * var(--ind-pe)));
	border-radius: calc(var(--ind-radius) * var(--ind-pe));
	box-shadow: 0 40px 100px -50px rgba(16,22,25, calc(.85 * var(--ind-pe)));
	will-change: clip-path;
}
/* Compensates the 100dvh cap: each page's .hero-copy already exceeds the
   room left after .photo-hero's own 154px/80px top/bottom padding at a
   900px-tall viewport (measured per page, not assumed identical — see the
   family doc), so without this the hero-stats row would be clipped off
   before the visitor ever scrolls. Same technique as the homepage's .hero
   .container — the overflow amount is measured live in JS per page
   (initIndHeroStage in main.js) and progressively revealed via translateY
   as --ind-pe advances, synced to the same scroll gesture that drives the
   shrink rather than needing a separate scroll cue. */
.ind-hero-stage .ind-hero .container {
	transform: translateY(calc(-1px * var(--ind-reveal, 0)));
	will-change: transform;
}

/* Additive header/#primary treatment — duplicates (not edits) the shared
   #primary:has(> :is(...)) / .site-header:has(~ #primary > :is(...))
   blocks above; see the comment block at the top of this section for why. */
#primary:has(> .ind-hero-stage) { padding-top: 0; }
.site-header:has(~ #primary > .ind-hero-stage):not(.is-day) .container {
	background: transparent; border-color: transparent; backdrop-filter: none; box-shadow: none;
}
.site-header:has(~ #primary > .ind-hero-stage):not(.is-day) .logo-swap .logo-img--color { opacity: 0; }
.site-header:has(~ #primary > .ind-hero-stage):not(.is-day) .logo-swap .logo-img--white { opacity: 1; }
/* Same scoping as the homepage's equivalent rules: light-on-dark for the
   bar's own links only (:not(.sub-menu a)), and :not(.is-open) so the
   mobile panel, solid white whatever the scroll state, never gets it. */
.site-header:has(~ #primary > .ind-hero-stage):not(.is-day):not(.is-open) .primary-nav a:not(.sub-menu a) { color: var(--lite-72); }
.site-header:has(~ #primary > .ind-hero-stage):not(.is-day):not(.is-open) .primary-nav a:hover:not(.sub-menu a),
.site-header:has(~ #primary > .ind-hero-stage):not(.is-day):not(.is-open) .primary-nav .current-menu-item > a:not(.sub-menu a) { color: #fff; }
.site-header:has(~ #primary > .ind-hero-stage):not(.is-day):not(.is-open) .primary-nav .sub-menu { background: var(--paper-hi); }
.site-header:has(~ #primary > .ind-hero-stage):not(.is-day) .menu-toggle { border-color: var(--hair-d); color: #EAF3F1; }
/* No matching .is-day padding-shrink pair here either — see the generic
   .site-header.is-day rule near the top of this file. */

/* Below 980px, the same fix the homepage's own #heroStage got (see that
   @media (max-width: 980px) block) and for the same reason: stacked, the
   copy+visual column is routinely taller than the sliver of scroll the
   pinned stage leaves to reveal it, so the headline sat invisible for a
   normal scroll's worth of distance on a real phone (this never showed
   up under prefers-reduced-motion, which already disabled the pin, so it
   went unnoticed until audited directly). Drops the pin, lets content
   flow at its natural height instead. Replaces the old 760px-only
   shrink-tuning that lived here -- moot once the pin itself is off at
   this width. initIndHeroStage() in main.js mirrors this with its own
   matchMedia check. */
@media (max-width: 61.25em) { /* 980px */
	.ind-hero-stage { height: auto; }
	.ind-hero-stage__sticky { position: static; height: auto; }
	.ind-hero-stage .ind-hero {
		clip-path: none !important; border-radius: 0 !important; box-shadow: none !important;
		height: auto; min-height: 100vh;
	}
	.ind-hero-stage .ind-hero .container { transform: none !important; }
}

@media (prefers-reduced-motion: reduce) {
	.ind-hero-stage { height: auto; }
	.ind-hero-stage__sticky { position: static; height: auto; }
	.ind-hero-stage .ind-hero {
		clip-path: none !important; border-radius: 0 !important; box-shadow: none !important;
		height: auto; min-height: 100vh;
	}
	.ind-hero-stage .ind-hero .container { transform: none !important; }
}

/* ==========================================================================
   About → "Our Services" summary cards (.discipline-grid)

   Two problems this fixes, both visible in the four-up row.

   1. Ragged bottom edge. .card is display:block, so each card's content
      flowed from the top and the .card-link landed wherever the content
      ended — 31px above the card's bottom edge on Engineering and Quality
      Assurance, 78px on Applied AI, 122px on Teams. A 91px spread across
      four equal-height cards. Making the card a flex column and pushing the
      whole tail down with margin-top:auto lands all four links on one
      baseline — the same pattern .blog-card, .product-card and
      .blog-teaser-card already use.

   2. Sub-links rendering as neither of their two intended designs.
      .card-sublinks is declared twice in this file: once around line 1231 as
      a wrapped row of pill chips, and again around line 3734 as a vertical
      bulleted list for the services hub. Same selector, same specificity, so
      the later rule wins layout — but only the properties it re-declares.
      padding, border-radius, background, border and font-weight from the
      chip rule were never overridden, so each link rendered as a full-width
      rounded button with a bullet dot sitting inside it. The resets below
      pick the vertical-list design deliberately and strip the chip remnants,
      scoped to this grid so the services and industry hubs are untouched.

   Alignment decision for the sub-links: they hang from a shared bottom
   anchor rather than a shared top one. Card heights are equal but pitch
   lengths and link counts are not, so top-aligning the lists would push the
   card-links back out of line — and the link baseline is what makes a card
   row read as finished. Subgrid then equalises the pitch and sub-link bands
   as well, so the hairline rules align too; the flex rules above it are the
   fallback where subgrid is unavailable.

   Row-gap tightened from the grid's default 22px to 14px, subgrid-only.
   Engineering's list needed a third entry (Business Intelligence belongs
   next to AI Native Product Engineering and Cloud Engineering — it's the
   third practice in that nav family) which grows the shared sub-link row
   track by one line, and because every card spans the same subgrid rows
   that growth lands on all four cards, not just Engineering's. 22px of
   row-gap runs on all four row boundaries (icon→title, title→description,
   description→sub-links, sub-links→link), so trimming it to 14px claws
   back 8px × 4 = 32px — enough to more than absorb the ~29px one more
   line costs, leaving every card 3px shorter than before rather than
   taller. Column-gap (the 22px between cards) is untouched: `gap` and
   `row-gap` are separate longhands, so this only overrides the vertical
   one. Left out of the flex fallback above deliberately, consistent with
   the rest of this block — subgrid is what turns row height into a
   shared budget across the row, so a non-subgrid browser has no growth
   to claw back from, and its icon/title/description spacing stays at the
   base .card-icon/.card h3 margins, unmodified. */
.discipline-grid .card { display: flex; flex-direction: column; }
.discipline-grid .card-sublinks { margin-top: auto; }
.discipline-grid .card-sublinks a {
	padding: 0; border: 0; border-radius: 0; background: none; font-weight: 500;
}

@supports (grid-template-rows: subgrid) {
	.discipline-grid { grid-template-rows: repeat(5, auto); row-gap: 0.875rem; }
	.discipline-grid .card { display: grid; grid-template-rows: subgrid; grid-row: span 5; }
	/* Back to a fixed offset: the subgrid rows now do the pushing, and an
	   auto margin inside a sized row would just re-introduce a gap. */
	.discipline-grid .card-sublinks { margin-top: 0.875rem; }
	.discipline-grid .card-link { align-self: end; justify-self: start; }
}

/* ==========================================================================
   Careers culture sets (theme 3) — #culture-theme-3, .cset, .cset-lb

   The careers page's culture section (theme 3 of its redesign). Everything
   is scoped to .cset and .cset-lb; its other pieces are the #culture-theme-3
   section, initCultureSets() in main.js and the culture-sets image folder.
   Every class, custom property and keyframe here starts cset-. The case
   studies own cs-, and the two once collided (see the header of that theme);
   tools/check-css-collisions.py fails if a class is defined in two sections.

   One set of photos -- a kind of moment, never a date -- fills the middle.
   Thin slices of the previous and next sets sit at either edge, and each
   slice shows one detail from its set's lead photo (the tree, the hands in
   the huddle, the temple tower): enough to tell what kind of moment is
   behind it, not enough to give the picture away. Picking a slice unfolds
   it -- the card widens into the middle, the lead photo zooms out from that
   detail to the whole scene, and the side photos open in as their column
   grows from zero -- while the set it replaces folds into the other edge.
   The first set is a single photo of the whole team and gets the whole
   panel to itself.

   Every one of those states is a CSS transition keyed off a single
   data-role on each set (before / prev / current / next / after); main.js
   only moves the attribute. Without JS the sets fall back to a plain
   scroll-snap row, each with its caption under it.

   Every photo is also a button into a full-size viewer (.cset-lb) that steps
   through its set, including a few photos the panel doesn't show.
   ========================================================================== */
.culture-sets { background: var(--popover); }
.cset {
	--cset-peek: 5.5rem;       /* width of an edge slice */
	--cset-gap: 1rem;        /* slice-to-panel gap */
	--cset-h: clamp(21.25rem, calc(100vh - 18.75rem), 31.25rem);
	--cset-meta: 7.375rem;      /* room under the panel for the set's title and caption */
	--cset-dur: .85s;
	--cset-ease: cubic-bezier(.22,1,.36,1);
	position: relative;
}

/* ---- Without JS: a swipeable row of whole sets -------------------------- */
.cset-track {
	display: flex; gap: var(--cset-gap);
	overflow-x: auto; scroll-snap-type: x mandatory; padding-bottom: 0.75rem;
}
.cset-set { flex: 0 0 88%; scroll-snap-align: start; }
.cset-grid {
	display: grid; height: var(--cset-h); gap: 0.5rem;
	grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr);
	border-radius: var(--radius-lg); overflow: hidden;
	transition: grid-template-columns var(--cset-dur) var(--cset-ease), gap var(--cset-dur) var(--cset-ease), border-radius var(--cset-dur) var(--cset-ease), filter .4s;
}
/* A set of one photo gives it the whole panel. */
.cset-set--solo .cset-grid { grid-template-columns: minmax(0, 1fr) minmax(0, 0fr); gap: 0; }
.cset-side { display: grid; gap: 0.5rem; min-width: 0; min-height: 0; transition: opacity .45s .3s; }
.cset-side--2 { grid-template-rows: repeat(2, minmax(0, 1fr)); }
.cset-side--3 { grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-rows: repeat(2, minmax(0, 1fr)); }
.cset-side--3 > :first-child { grid-column: 1 / -1; }
.cset-tile { position: relative; margin: 0; min-width: 0; min-height: 0; overflow: hidden; border-radius: 0.625rem; background: var(--muted); }
.cset-tile img {
	width: 100%; height: 100%; object-fit: cover;
	object-position: var(--fx, 50%) var(--fy, 50%);
	transform-origin: var(--gx, 50%) var(--gy, 50%);
	transition: transform var(--cset-dur) var(--cset-ease), object-position var(--cset-dur) var(--cset-ease);
}
/* A row of people standing (.cset-tile--stand). A wide panel shows the
   photo's full width, so all it chooses is which band of the height to
   show, and only a few bands cut the people cleanly: head to toe, or head
   to mid-thigh. Anything between slices through knees, ankles or whatever
   they are holding. So the photo names its lines, as fractions of its
   height (and --fa, its own width / height): --ft just above the heads, and
   --fb, the line it stands on -- the panel's bottom edge. Two more are
   optional: --fbw, a higher line for a panel too flat to reach up to --ft
   from --fb, and --fbt, a lower one for a panel too tall to stand on --fb
   at all (there isn't that much photo above it). A fixed object-position
   can't hold a line: its crop slides as the panel's shape changes with the
   window's height. Where the maths isn't supported the photo keeps its
   plain --fy crop. */
@supports (top: calc(1px * tan(atan2(1cqh, 1cqw)))) {
	.cset-tile--stand { container-type: size; }
	.cset-tile--stand img {
		/* The share of the photo's height the panel shows. tan(atan2()) is
		   how CSS divides one length by another. */
		--cset-seen: calc(var(--fa) * tan(atan2(100cqh, 100cqw)));
		/* --fb if that share reaches from --fb up to --ft, otherwise --fbw;
		   then --fbt if the share is more than there is above --fb. The large
		   factor turns each difference into a switch. */
		--cset-flat: clamp(var(--fbw, var(--fb)), var(--fbw, var(--fb)) + (var(--cset-seen) - var(--fb) + var(--ft)) * 9999, var(--fb));
		--cset-line: clamp(var(--cset-flat), var(--cset-flat) + (var(--cset-seen) - var(--fb)) * 9999, var(--fbt, var(--cset-flat)));
		/* Put that line on the panel's bottom edge, but never push the heads
		   out of the top. */
		object-position: var(--fx, 50%) clamp(0%, min(var(--cset-line) - var(--cset-seen), var(--ft)) / max(1 - var(--cset-seen), .0001) * 100%, 100%);
	}
}
/* Each photo is the button that opens it in the viewer. */
.cset-open { position: relative; display: block; width: 100%; height: 100%; padding: 0; }
.cset-open:focus-visible { outline: 2px solid #fff; outline-offset: -6px; }
.cset-meta { margin-top: 1.125rem; max-width: 62ch; }
/* A set's own title, with its own typography. While the carousel shared the
   case studies' .cs-title name, this rule only set a size and the rest of
   the look came from theirs -- and the size leaked back onto every
   case-study headline. The two can't see each other now. */
.cset-title {
	font-family: var(--font-display); font-variation-settings: 'wdth' 84, 'wght' 760;
	font-size: 1.5rem; line-height: 1.03; letter-spacing: -.021em; max-width: 22ch; color: var(--ink);
}
.cset-cap { margin-top: 0.375rem; color: var(--muted-foreground); line-height: 1.55; }
.cset-peek[hidden], .cset-nav[hidden] { display: none; }

/* ---- Carousel (main.js adds .is-ready) ---------------------------------- */
.cset.is-ready .cset-stage {
	position: relative; height: var(--cset-h); margin-bottom: var(--cset-meta);
	container-type: inline-size; touch-action: pan-y;
}
.cset.is-ready .cset-track { display: block; position: absolute; inset: 0; overflow: visible; padding: 0; }
.cset.is-ready .cset-set {
	position: absolute; top: 0; height: 100%;
	transition: left var(--cset-dur) var(--cset-ease), width var(--cset-dur) var(--cset-ease), opacity .5s;
}
.cset.is-ready .cset-grid { height: 100%; }
.cset.is-ready .cset-set[data-role="current"] { left: calc(var(--cset-peek) + var(--cset-gap)); width: calc(100% - 2 * (var(--cset-peek) + var(--cset-gap))); }
.cset.is-ready .cset-set[data-role="prev"] { left: 0; width: var(--cset-peek); }
.cset.is-ready .cset-set[data-role="next"] { left: calc(100% - var(--cset-peek)); width: var(--cset-peek); }
.cset.is-ready .cset-set[data-role="before"] { left: 0; width: 0; opacity: 0; }
.cset.is-ready .cset-set[data-role="after"] { left: 100%; width: 0; opacity: 0; }
/* A set wrapping round the back (before <-> after) jumps instead of
   sweeping across the whole stage behind the others. */
.cset.is-ready .cset-set.no-anim,
.cset.is-ready .cset-set.no-anim * { transition: none !important; }

/* A slice is just the lead photo, zoomed on its detail and muted. */
.cset.is-ready .cset-set:not([data-role="current"]) .cset-grid {
	grid-template-columns: minmax(0, 1fr) minmax(0, 0fr); gap: 0;
	border-radius: calc(var(--cset-peek) / 2);
	filter: saturate(.55) brightness(.88);
}
.cset.is-ready .cset-set:not([data-role="current"]) .cset-side { opacity: 0; transition-delay: 0s; }
.cset.is-ready .cset-set:not([data-role="current"]) .cset-tile--lead img {
	object-position: var(--gx, 50%) var(--gy, 50%);
	transform: scale(var(--gz, 1.3));
}

/* ---- The wall: a set whose photos are all one size ----------------------
   Four across and two down, for a set where no photo should outrank
   another: on awards night every award gets the same tile. The markup is
   the usual lead photo plus a side column, but the column dissolves
   (display: contents) so all eight tiles sit on the one grid. The first
   photo is the lead only for the edge slice, which folds the grid down to
   its first cell. The tracks are written out, not repeat()ed, so the two
   states are lists a browser can animate between. */
.cset-set--wall .cset-grid {
	grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr);
	grid-template-rows: minmax(0, 1fr) minmax(0, 1fr);
	transition: grid-template-columns var(--cset-dur) var(--cset-ease), grid-template-rows var(--cset-dur) var(--cset-ease), gap var(--cset-dur) var(--cset-ease), border-radius var(--cset-dur) var(--cset-ease), filter .4s;
}
.cset-set--wall .cset-side { display: contents; }
/* The column's fade has nothing to act on once the column has dissolved,
   so its tiles carry it themselves -- and sooner than a side column does.
   There the wait hides photos squeezed into a sliver of a column; here it
   would leave seven-eighths of the panel empty around one small photo. */
.cset-set--wall .cset-side > .cset-tile { transition: opacity .4s .1s; }
.cset.is-ready .cset-set--wall:not([data-role="current"]) .cset-grid {
	grid-template-columns: minmax(0, 1fr) minmax(0, 0fr) minmax(0, 0fr) minmax(0, 0fr);
	grid-template-rows: minmax(0, 1fr) minmax(0, 0fr);
}
.cset.is-ready .cset-set--wall:not([data-role="current"]) .cset-side > .cset-tile { opacity: 0; transition-delay: 0s; }
/* A wall tile is whatever shape the screen leaves it: half as wide again as
   it is tall on a short laptop, taller than wide on a tablet. Each award
   photo is two people and a trophy, and a tall tile can't hold all three,
   so there it turns to --fxn, the crop that keeps the award-winner and the
   trophy. One object-position can't serve both: the spot that centres two
   faces in a square tile pushes one of them out of a narrow one. */
.cset-set--wall .cset-tile { container-type: size; }
@container (max-aspect-ratio: 19/20) {
	.cset-set--wall .cset-tile img { object-position: var(--fxn, var(--fx, 50%)) var(--fy, 50%); }
}

/* The current set's photos invite a click: a zoom cursor, and an enlarge
   mark in the corner of the one under the pointer or keyboard focus. */
.cset.is-ready .cset-set[data-role="current"] .cset-open { cursor: zoom-in; }
.cset.is-ready .cset-set[data-role="current"] .cset-open::after {
	content: ''; position: absolute; top: 0.75rem; right: 0.75rem; width: 2.25rem; height: 2.25rem; border-radius: 50%;
	background: rgba(16,22,25,.62) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='15 3 21 3 21 9'/%3E%3Cpolyline points='9 21 3 21 3 15'/%3E%3Cline x1='21' y1='3' x2='14' y2='10'/%3E%3Cline x1='3' y1='21' x2='10' y2='14'/%3E%3C/svg%3E") center / 1rem no-repeat;
	opacity: 0; transform: scale(.8); transition: opacity .2s, transform .25s var(--cset-ease);
	pointer-events: none;
}
.cset.is-ready .cset-set[data-role="current"] .cset-open:is(:hover, :focus-visible)::after { opacity: 1; transform: none; }

/* The set's title and caption hang under the panel, lined up with its
   left edge, and only the current set's are shown. */
.cset.is-ready .cset-meta {
	position: absolute; top: calc(100% + 1.375rem); left: 0; margin: 0;
	width: min(62ch, calc(100cqw - 2 * (var(--cset-peek) + var(--cset-gap))));
	opacity: 0; visibility: hidden; transition: opacity .25s, visibility 0s .25s;
}
.cset.is-ready .cset-set[data-role="current"] .cset-meta { opacity: 1; visibility: visible; transition: opacity .45s .35s, visibility 0s; }

/* The edge slices' buttons sit over them: the set's name down the slice,
   an arrow once it's pointed at or focused. */
.cset-peek {
	position: absolute; top: 0; z-index: 2; height: 100%; width: var(--cset-peek);
	display: flex; flex-direction: column; align-items: center; justify-content: flex-end;
	padding: 0 0 1.375rem; border-radius: calc(var(--cset-peek) / 2);
	color: #fff; transition: width var(--cset-dur) var(--cset-ease);
}
.cset-peek--prev { left: 0; }
.cset-peek--next { right: 0; }
.cset-peek::before {
	content: ''; position: absolute; inset: 42% 0 0; z-index: -1;
	border-radius: 0 0 calc(var(--cset-peek) / 2) calc(var(--cset-peek) / 2);
	background: linear-gradient(rgba(16,22,25,0), rgba(16,22,25,.72));
}
.cset-peek-label {
	writing-mode: vertical-rl; transform: rotate(180deg); white-space: nowrap;
	font-family: var(--font-display); font-variation-settings: 'wdth' 112, 'wght' 700;
	font-size: 1rem; letter-spacing: .01em;
}
.cset-peek-arrow {
	position: absolute; top: 50%; left: 50%; width: 2.375rem; height: 2.375rem; margin: -1.1875rem 0 0 -1.1875rem;
	display: grid; place-items: center; border-radius: 50%;
	background: #fff; color: var(--ink);
	opacity: 0; transform: scale(.7); transition: opacity .25s, transform .3s var(--cset-ease);
}
.cset-peek:hover .cset-peek-arrow,
.cset-peek:focus-visible .cset-peek-arrow { opacity: 1; transform: none; }
.cset-peek:focus-visible { border-radius: calc(var(--cset-peek) / 2); outline-offset: 3px; }
/* Pointing at a slice brings its colour back and eases it out a little. */
.cset:has(.cset-peek--prev:is(:hover, :focus-visible)) .cset-set[data-role="prev"] .cset-grid,
.cset:has(.cset-peek--next:is(:hover, :focus-visible)) .cset-set[data-role="next"] .cset-grid { filter: none; }
@media (hover: hover) {
	.cset-peek:hover { width: calc(var(--cset-peek) + 0.75rem); }
	.cset:has(.cset-peek--prev:hover) .cset-set[data-role="prev"] { width: calc(var(--cset-peek) + 0.75rem); }
	.cset:has(.cset-peek--next:hover) .cset-set[data-role="next"] { left: calc(100% - var(--cset-peek) - 0.75rem); width: calc(var(--cset-peek) + 0.75rem); }
}

/* Set buttons and Pause/Play, lined up with the panel. */
.cset-nav {
	display: flex; align-items: center; flex-wrap: wrap; gap: 0.625rem 0.875rem;
	padding-left: calc(var(--cset-peek) + var(--cset-gap));
}
.cset-toggle {
	display: inline-flex; align-items: center; gap: 0.5rem; height: 2.5rem; padding: 0 1rem 0 0.8125rem;
	border-radius: 999px; border: 1px solid var(--hair-l); color: var(--ink-70);
	font-size: .88rem; font-weight: 600; transition: background .2s, color .2s;
}
.cset-toggle:hover { background: var(--paper-hi); color: var(--ink); }
.cset-icon-play { display: none; }
.cset:not(.is-playing) .cset-icon-play { display: block; }
.cset:not(.is-playing) .cset-icon-pause { display: none; }
.cset-chips { display: flex; flex-wrap: wrap; gap: 0.5rem; flex: 1 1 0; min-width: min(100%, 17.5rem); }
.cset-chip {
	position: relative; overflow: hidden; height: 2.5rem; padding: 0 1rem; border-radius: 999px;
	border: 1px solid var(--hair-l); background: var(--paper-hi); color: var(--ink);
	font-size: .9rem; font-weight: 600; transition: background .25s, color .25s, border-color .25s;
}
.cset-chip:hover { border-color: var(--teal-deep); }
.cset-chip[aria-current="true"] { background: var(--teal-deep); border-color: var(--teal-deep); color: #fff; }
.cset-chip:focus-visible,
.cset-toggle:focus-visible { border-radius: 999px; }
/* Time left before the next set, drawn along the foot of the current chip. */
.cset-chip::after {
	content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 0.1875rem;
	background: rgba(255,255,255,.7); transform: scaleX(0); transform-origin: left;
}
.cset.is-playing .cset-chip[aria-current="true"]::after { animation: cset-countdown var(--cset-interval, 6500ms) linear forwards; }
.cset.is-paused .cset-chip[aria-current="true"]::after { animation-play-state: paused; }
@keyframes cset-countdown { to { transform: scaleX(1); } }
@keyframes cset-fade { from { opacity: 0; } }

/* ---- The viewer (#cset-viewer) ---------------------------------------------
   Native <dialog>, like the job-description modal: showModal() puts it in
   the top layer, Escape closes it and focus goes back to the photo that
   opened it. It fills the window, and main.js sizes the photo to the space
   between the bar and the caption, so the width it asks srcset for is the
   width it's drawn at. The photo itself grows out of the tile that was
   clicked and shrinks back into it on close (main.js, when the tile is on
   show); the backdrop and controls just fade around it. */
.cset-lb {
	width: 100vw; height: 100vh; height: 100dvh; max-width: 100vw; max-height: 100dvh;
	margin: 0; padding: 0; border: 0; overflow: hidden;
	background: transparent; color: #fff;
	touch-action: none; /* swipes are the viewer's own; nothing behind it scrolls */
}
.cset-lb::backdrop { background: rgba(7,20,24,.96); transition: opacity .3s ease; }
.cset-lb[open]::backdrop { animation: cset-fade .3s ease-out; }
.cset-lb.is-closing::backdrop { opacity: 0; }
.cset-lb-inner { display: grid; grid-template-rows: auto minmax(0, 1fr) auto; height: 100%; }
.cset-lb-bar { display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: 1rem 1.25rem 0.75rem 1.75rem; }
.cset-lb-head { display: flex; align-items: baseline; gap: 0.875rem; min-width: 0; }
.cset-lb-title {
	font-family: var(--font-display); font-variation-settings: 'wdth' 112, 'wght' 740;
	font-size: 1.45rem; letter-spacing: -.02em;
}
.cset-lb-count { font-size: .92rem; color: var(--lite-72); font-variant-numeric: tabular-nums; }
.cset-lb-stage { position: relative; display: flex; align-items: center; justify-content: center; min-height: 0; padding: 0 6rem; }
.cset-lb-img { display: block; border-radius: 0.625rem; transition: opacity .25s ease, transform .35s var(--cset-ease); }
/* Stepping, a new photo slides in from the side the visitor stepped
   towards (--cset-lb-dir is -1 or 1). */
.cset-lb-img.is-loading { opacity: 0; transform: translateX(calc(var(--cset-lb-dir, 0) * 1.75rem)); transition: none; }
.cset-lb-cap { padding: 0.875rem 1.75rem 1.625rem; min-height: 1.5em; text-align: center; font-size: .98rem; line-height: 1.5; color: var(--lite-72); }
.cset-lb-btn {
	display: grid; place-items: center; width: 3.25rem; height: 3.25rem; border-radius: 50%;
	background: rgba(234,243,241,.1); color: #fff; transition: background .2s, opacity .25s ease;
}
.cset-lb-btn:hover { background: rgba(234,243,241,.22); }
.cset-lb-btn:focus-visible { outline: 2px solid var(--teal); outline-offset: 3px; border-radius: 50%; }
.cset-lb-btn[hidden] { display: none; }
.cset-lb-close { width: 2.875rem; height: 2.875rem; flex: none; }
.cset-lb-prev, .cset-lb-next { position: absolute; top: 50%; margin-top: -1.625rem; }
.cset-lb-prev { left: 1.5rem; }
.cset-lb-next { right: 1.5rem; }
.cset-lb-bar, .cset-lb-cap { transition: opacity .25s ease; }
.cset-lb[open] :is(.cset-lb-bar, .cset-lb-cap, .cset-lb-prev, .cset-lb-next) { animation: cset-fade .35s ease-out; }
.cset-lb.is-closing :is(.cset-lb-bar, .cset-lb-cap, .cset-lb-prev, .cset-lb-next) { opacity: 0; }

@media (max-width: 61.25em) { /* 980px */
	.cset { --cset-peek: 3.5rem; --cset-gap: 0.75rem; --cset-h: clamp(22.5rem, 54vh, 28.75rem); --cset-meta: 7.75rem; }
	.cset-grid { grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr); }
	.cset-peek-label { font-size: .9rem; }
}
/* Phones: lead photo on top, the rest under it; slices down to a sliver. */
@media (max-width: 40em) { /* 640px */
	.cset { --cset-peek: 1.875rem; --cset-gap: 0.5rem; --cset-h: clamp(26.25rem, 68vh, 32.5rem); --cset-meta: 9.375rem; }
	.cset-grid {
		grid-template-columns: minmax(0, 1fr); grid-template-rows: minmax(0, 1.05fr) minmax(0, 1fr);
		gap: 0.375rem; border-radius: var(--radius);
		transition: grid-template-rows var(--cset-dur) var(--cset-ease), gap var(--cset-dur) var(--cset-ease), border-radius var(--cset-dur) var(--cset-ease), filter .4s;
	}
	.cset-set--solo .cset-grid { grid-template-columns: minmax(0, 1fr); grid-template-rows: minmax(0, 1fr) minmax(0, 0fr); gap: 0; }
	.cset-side { gap: 0.375rem; }
	.cset-side--2 { grid-template-rows: none; grid-template-columns: repeat(2, minmax(0, 1fr)); }
	.cset.is-ready .cset-set:not([data-role="current"]) .cset-grid { grid-template-columns: minmax(0, 1fr); grid-template-rows: minmax(0, 1fr) minmax(0, 0fr); }
	/* The wall stands the other way up: two across, four down. */
	.cset-set--wall .cset-grid {
		grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
		grid-template-rows: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr);
	}
	.cset.is-ready .cset-set--wall:not([data-role="current"]) .cset-grid {
		grid-template-columns: minmax(0, 1fr) minmax(0, 0fr);
		grid-template-rows: minmax(0, 1fr) minmax(0, 0fr) minmax(0, 0fr) minmax(0, 0fr);
	}
	.cset-tile { border-radius: 0.5rem; }
	.cset.is-ready .cset-set[data-role="current"] .cset-open::after { display: none; }
	.cset-peek { padding-bottom: 0.875rem; }
	.cset-peek-label { font-size: var(--text-min-sans); }
	.cset-peek-arrow { display: none; }
	.cset.is-ready .cset-meta { left: calc(-1 * (var(--cset-peek) + var(--cset-gap))); width: 100cqw; }
	.cset-nav { padding-left: 0; }
	.cset-title { font-size: 1.3rem; }
	.cset-toggle, .cset-chip { height: 2.375rem; }
	.cset-lb-bar { padding: 0.75rem 0.75rem 0.625rem 1.125rem; }
	.cset-lb-title { font-size: 1.2rem; }
	.cset-lb-stage { padding: 0 0.5rem; }
	.cset-lb-prev, .cset-lb-next { width: 2.75rem; height: 2.75rem; margin-top: -1.375rem; background: rgba(16,22,25,.55); }
	.cset-lb-prev { left: 0.875rem; }
	.cset-lb-next { right: 0.875rem; }
	.cset-lb-cap { padding: 0.75rem 1.125rem 1.375rem; font-size: .92rem; }
}
/* A panel narrower than the photo would crop the team photo at its sides
   and lose whoever stands there, so the photo is shown whole instead, over
   a blurred, dimmed copy of itself. That is every phone, where the panel is
   portrait, and an upright tablet, where it comes out about square. (From
   800px up the panel is always wider than a 4:3 photo.) The slice still
   crops it like any other. */
@media (max-width: 41.5em), (max-width: 50em) and (orientation: portrait) { /* 664px, 800px */
	.cset-set--solo .cset-tile--lead::before {
		content: ''; position: absolute; inset: -1.5rem;
		background: var(--solo-bg) center / cover; filter: blur(18px) brightness(.6);
	}
	.cset-set--solo .cset-tile--lead img { object-fit: contain; object-position: 50% 50%; }
	.cset.is-ready .cset-set--solo:not([data-role="current"]) .cset-tile--lead img { object-fit: cover; }
}
@media (prefers-reduced-motion: reduce) {
	.cset.is-ready .cset-set, .cset-grid, .cset-side, .cset-set--wall .cset-side > .cset-tile, .cset-tile img, .cset.is-ready .cset-meta, .cset-peek, .cset-lb-img { transition: none !important; }
	.cset.is-ready .cset-set[data-role="current"] { animation: cset-fade .3s ease-out; }
	.cset-lb[open]::backdrop, .cset-lb[open] :is(.cset-lb-bar, .cset-lb-cap, .cset-lb-prev, .cset-lb-next) { animation: none; }
}

/* ==========================================================================
   Open-jobs — job-description modal (proof of concept, one card only)

   Native <dialog>, not a hand-rolled overlay div: showModal() promotes it
   to the browser's top layer, which is what lets it sit above everything
   — including .role-card and .card, which set overflow:hidden and would
   clip a position:fixed child of theirs — without a z-index arms race.
   ::backdrop is the dialog's own pseudo-element, dimming the page behind
   it; Escape-to-close is native too. The only bit that isn't free is a
   backdrop click closing the dialog, wired in JS.

   Trigger styling: .role-card-title-btn and .role-card-jd-link both reset
   to `all:unset` first because a <button> otherwise arrives with its own
   font, padding and border — easier to opt back into exactly the two or
   three properties each needs than to override the rest of the UA sheet.
   ========================================================================== */
.role-card-title-btn { all: unset; cursor: pointer; display: block; }
.role-card-title-btn h3 { transition: color .2s ease; }
.role-card-title-btn:hover h3, .role-card-title-btn:focus-visible h3 { color: var(--teal-deep); }
.role-card-title-btn:focus-visible { outline: 2px solid var(--teal); outline-offset: 4px; border-radius: 0.25rem; }
.role-card-jd-link {
	all: unset; cursor: pointer; display: inline-flex; align-items: center; gap: 0.3125rem;
	margin-top: 0.625rem; font-size: .84rem; font-weight: 600; color: var(--teal-deep);
}
.role-card-jd-link svg { transition: transform .2s ease; }
.role-card-jd-link:hover, .role-card-jd-link:focus-visible { text-decoration: underline; }
.role-card-jd-link:hover svg, .role-card-jd-link:focus-visible svg { transform: translateX(2px); }
.role-card-jd-link:focus-visible { outline: 2px solid var(--teal); outline-offset: 3px; border-radius: 0.25rem; }

.jd-modal {
	padding: 0; border: 0; border-radius: var(--radius-lg); overflow: hidden;
	/* max-width neutralizes the UA stylesheet's own dialog:modal max-width
	   (which otherwise wins over the width below and clips it) — see the
	   520px block for the size that actually surfaced it. */
	width: min(42.5rem, calc(100vw - 3rem)); max-width: 100vw;
	max-height: min(45rem, calc(100vh - 5rem));
	background: var(--card); color: var(--foreground); box-shadow: var(--shadow-soft);
}
.jd-modal::backdrop { background: rgba(7,26,32,.6); backdrop-filter: blur(2px); }
.jd-modal-inner { display: flex; flex-direction: column; height: 100%; max-height: inherit; }
.jd-modal-head {
	display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem;
	padding: 1.5rem 1.75rem; border-bottom: 1px solid var(--border); flex: none;
}
.jd-modal-head h3 { font-size: 1.25rem; margin-bottom: 0.5rem; }
.jd-modal-close {
	all: unset; flex: none; width: 2.25rem; height: 2.25rem; border-radius: 999px;
	border: 1px solid var(--border); background: var(--popover); color: var(--foreground);
	display: flex; align-items: center; justify-content: center; cursor: pointer;
	transition: border-color .2s ease, color .2s ease;
}
.jd-modal-close:hover, .jd-modal-close:focus-visible { border-color: var(--teal); color: var(--teal-deep); }
.jd-modal-close:focus-visible { outline: 2px solid var(--teal); outline-offset: 2px; }
.jd-modal-body { padding: 1.375rem 1.75rem; overflow-y: auto; flex: 1 1 auto; }
.jd-modal-body h4 { font-size: .95rem; margin: 1.375rem 0 0.625rem; font-variation-settings: 'wdth' 108, 'wght' 680; }
.jd-modal-body h4:first-child { margin-top: 0; }
.jd-modal-body > p { color: var(--muted-foreground); line-height: 1.6; }
.jd-modal-body ul { margin: 0; padding-left: 1.25rem; color: var(--muted-foreground); line-height: 1.65; }
.jd-modal-body li + li { margin-top: 0.375rem; }
.jd-modal-foot { padding: 1rem 1.75rem; border-top: 1px solid var(--border); flex: none; display: flex; justify-content: flex-end; }

@media (max-width: 32.5em) { /* 520px */
	/* max-width/max-height set explicitly alongside width/height, not just
	   for symmetry — Chrome's UA stylesheet gives dialog:modal its own
	   default max-width/max-height (a shave-a-few-px-off-the-viewport
	   value), and max-width always wins over width in the box model, so
	   without this override the dialog rendered ~40px narrower than the
	   100vw asked for below, off-center inside the extra space. */
	.jd-modal { width: 100vw; max-width: 100vw; max-height: 100vh; height: 100vh; border-radius: 0; }
	.jd-modal-head, .jd-modal-body, .jd-modal-foot { padding-left: 1.25rem; padding-right: 1.25rem; }
	.jd-modal-foot .btn { width: 100%; justify-content: center; }
}

/* ==========================================================================
   Staff Augmentation — "the placement" hero panel + "the bench, at rest"
   canvas. Shipped live, promoted from a design-test build that was reviewed,
   approved, and later removed once it had served its purpose — see
   docs/design/staff-augmentation-hero-canvas-prompt.md, Revisions 2-6, for
   the full design history. The original .roster + #teams-hero-bench-bg hero
   was kept at services/staff-augmentation-design-unique/ until that page, and
   the rules only it used, were removed on 2026-10-26 (Revision 6).

   Why this exists: every other service hero pairs a photo or flat ink frame
   with a LABELED panel a reader can just read — .card-run on Quality
   Engineering ("functional 1.1m", "All checks passed"), .rag-panel on
   Quantitative Analytics Testing ("Binomial model: within 0.01% tolerance").
   Staff Augmentation was the only one whose hero visual asked to be decoded
   instead: 18 circles carrying 7 role icons, of which a reader can reliably
   name maybe three (code, shield, infinity). A visual that invites decoding
   and then doesn't reward it reads worse than a plain one, and this page in
   particular has a specific, countable thing to say instead — its own
   process, four real steps in order.

   The reviewed build originally dated each step ("Day 1" ... "Day 8", "8
   days" total). This shipped build carries the same four steps with the
   day-by-day timeline removed — a specific delivery SLA the business isn't
   in a position to guarantee for every placement — replaced with a plain
   step count. See Revision 4 for the reasoning.
   ========================================================================== */
#teams-hero-seats-bg { position: absolute; inset: 0; z-index: 0; pointer-events: none; display: block; }

.card-placement {
	display: flex; flex-direction: column;
	padding: 1.375rem 1.5rem; border: 1px solid var(--border); border-radius: var(--radius);
	background: rgba(4,10,14,.6);
}
.card-placement-head {
	display: flex; align-items: baseline; justify-content: space-between; gap: 0.875rem;
	padding-bottom: 0.875rem; border-bottom: 1px solid rgba(234,243,241,.12);
	font-family: var(--font-mono); font-size: var(--text-min); letter-spacing: .12em; text-transform: uppercase;
	color: rgba(234,243,241,.5);
}
.card-placement-total {
	flex: none; color: #EAF3F1; letter-spacing: .06em; font-variant-numeric: tabular-nums;
}

/* The steps are a real sequence — a requirement precedes a shortlist precedes
   an interview precedes a start date — so an ordered list with a rail earns
   its numbering here, unlike a decorative 01/02/03. */
.placement-steps { list-style: none; margin: 0; padding: 1.125rem 0 0; position: relative; }
.placement-steps::before {
	content: ''; position: absolute; left: 0.3125rem; top: 1.625rem; bottom: 1.5rem; width: 1px;
	background: rgba(234,243,241,.16);
}
.placement-step { position: relative; padding: 0 0 1.125rem 1.625rem; }
.placement-step:last-child { padding-bottom: 0.25rem; }
.placement-step::before {
	content: ''; position: absolute; left: 0; top: 0.25rem; width: 0.6875rem; height: 0.6875rem;
	border-radius: 50%; background: var(--hero-ink);
	border: 1.5px solid rgba(234,243,241,.36);
}
.placement-day {
	display: block; font-family: var(--font-mono); font-size: var(--text-min); letter-spacing: .14em;
	text-transform: uppercase; color: rgba(234,243,241,.62);
}
.placement-what { display: block; margin-top: 0.1875rem; font-size: .9rem; line-height: 1.45; color: var(--lite-72); }

/* The one step the client owns gets the family accent, so "you interview" is
   the thing the eye lands on second, after the step marker. */
.placement-step--yours::before { border-color: var(--teal); background: rgba(93,160,172,.28); }
.placement-step--yours .placement-day { color: var(--teal); }

/* Arrival: solid, not outlined — the only filled node on the rail. */
.placement-step--done::before { background: var(--butter); border-color: var(--butter); }
.placement-step--done .placement-day { color: var(--butter); }
.placement-step--done .placement-what { color: #EAF3F1; font-weight: 500; }

@media (max-width: 45em) { /* 720px */
	.card-placement { padding: 1.125rem 1.125rem; }
	.placement-what { font-size: .86rem; }
}

/* --- the photo-led hero visual --------------------------------------------
   Shipped live (promoted from design test 2, since removed -- see
   docs/design/staff-augmentation-hero-canvas-prompt.md, Revisions 4-5).
   .card-placement, stacked UNDER a photograph rather than floated over one. Every sibling service page floats
   its panel over a photo (.svc-hero-visual--photo), and that is right for
   their images, which are atmospheric textures - a dark server room, a
   blurred desk - where it does not matter what the panel covers.

   This photo is of people, on the page that sells people, and the group fills
   the left ~72% of the frame. Floating a 400px panel over a 576px box leaves
   the photo a 148px vertical slice of a wide group shot: partial faces, which
   read as an accident rather than a crop. Measured both ways before choosing.

   Stacked, the photo gets a proper landscape frame at its own native ratio
   and the panel gets the full box width - which also lets every step sit on
   one line instead of wrapping, so the process list reads faster than it did
   in design test 1. The photo carries only a light top-to-bottom gradient
   (.12 -> .55) because no text sits on it; it just needs to belong to the
   dark hero rather than glow out of it.
   ------------------------------------------------------------------------ */
.svc-hero-visual--stack { display: flex; flex-direction: column; gap: 0.875rem; padding: 0.875rem; }
.stack-photo {
	border-radius: calc(var(--radius) - 0.3125rem);
	aspect-ratio: 16 / 7;
	background-image: linear-gradient(180deg, rgba(9,14,20,.12) 0%, rgba(9,14,20,.55) 100%), var(--hero-photo);
	background-size: cover;
	/* faces sit in the upper-middle band of the source crop */
	background-position: center 38%;
}
.svc-hero-visual--stack .card-placement { background: transparent; border: 0; padding: 0.375rem 0.625rem 0.625rem; }
/* The 16:7 frame is right on desktop, where the photo is 548px wide and four
   faces read comfortably. At 299px it letterboxes them down to ~131px tall and
   the faces get small enough to stop doing their job, so the frame gets taller
   rather than the photo getting wider. */
@media (max-width: 45em) { /* 720px */
	.stack-photo { aspect-ratio: 3 / 2; }
}

/* ==========================================================================
   Service heroes on short desktop windows — the eight side-by-side practice
   heroes (Agentic AI, Generative AI, AI Native Product Engineering, Cloud
   Engineering, Business Intelligence, Quality Engineering, Quantitative
   Analytics Testing, Staff Augmentation).

   Every rule above sizes these heroes by width, and from ~1280px wide up
   all of them have already hit their ceilings — so a 15.6" laptop (~727px
   of usable height) and an 11.6" one (~640px at 1366x768) get the exact
   same layout, the smaller one just has ~90px less room below it. Measured
   at 1366x640, the primary CTA sat below the fold on six of the eight
   pages, by 19-79px: a three-line headline (Agentic AI, Gen AI) or a
   seven-to-nine-line lede (BI, QE, AI Native) is more than fits under the
   ~250px each hero spends on the header gap, breadcrumb and eyebrow. That
   top ~250px is deliberately left alone here — it's what keeps these pages
   consistent with one another — so the room comes from the headline, the
   lede, the two gaps around the lede, and the CTA buttons.

   The block only exists below 720px tall, so a taller window renders
   exactly as before. Inside it, every length ramps linearly from its
   normal size at 720px down to a floor at 600px (written as a·vh + b, the
   same way the homepage hero's short-viewport ramp is), so nothing snaps
   on the way down. The headline keeps its width term too — min(vw, vh) —
   so whichever dimension is tighter decides, and it's the one length that
   keeps shrinking below 600px, down to the same minimum its width rule
   already had. Measured after, at 1280px+ wide: every CTA fits with 27px+
   to spare at 640px tall and 15px+ at 600px. Narrower-and-short windows
   (~1024-1150px wide, e.g. a tablet in landscape) gain 40-57px but can
   still need a scroll on the longest pages — there the ~460-530px copy
   column, not the height, is what makes the lede run 8-10 lines.

   Two changes are switches rather than ramps, and both flip together at
   the same 720px boundary:
   - The text column is pinned to the top; the picture keeps .split's
     centring. Pinning the text matters where the picture is the taller
     column (BI, Staff Augmentation): centring already sits their
     breadcrumb lower than the other pages', and shrinking the copy only
     pushes it further — trialled on Agentic AI's old 4:5 photo, its
     breadcrumb slid from 175px to 220px and took half the saving with it.
     The picture is
     deliberately not pinned as well: top-aligned, it started level with
     the breadcrumb, a full headline-height above the h1, and got read
     before the headline did. Centred, it lands 25-74px above the headline
     at 640px and level with it by ~700px, close to where it sits on a
     taller window. A picture taller than the text fills the row, so it
     still starts at the top — the same as on a taller window.
   - A 60ch measure (from 52-54ch / 520px) and a 1.6 line height (from 1.7
     on .svc-hero-copy) on the lede, so it wraps into fewer, slightly
     tighter lines — still inside the comfortable 45-75 character range.
   Pictures are deliberately not resized to fit the window. At these
   heights several run past the bottom edge — Gen AI's, Agentic AI's and
   QE's captions clip at ~600px — which is accepted: the rest of the picture is a scroll
   away, and so is everything below the CTA that the hero is leading into.

   Range syntax rather than max-height: 719px on purpose: under 125%/150%
   display scaling (most Windows laptops) the viewport height is often
   fractional, and a 719.2px window fell between max-height: 719px and
   720px and skipped this block entirely — measured, not hypothetical.
   width > 980px is likewise the exact complement of the max-width: 980px
   stacked-hero breakpoint. */
@media (width > 61.25em) and (height < 45em) { /* 980px, 720px */
	:is(.genai-hero, .cloud-hero, .bi-hero, .qa-hero, .ainative-hero, .teams-hero, .agentic-atmosphere > .photo-hero) .split > .hero-copy { align-self: start; }

	:is(.genai-hero, .cloud-hero, .bi-hero, .qa-hero, .ainative-hero, .teams-hero, .agentic-atmosphere > .photo-hero) .hero-copy h1 { font-size: clamp(2.25rem, min(4.3vw, 8.667vh - 0.5rem), 3.4rem); }

	:is(.genai-hero, .cloud-hero, .bi-hero, .qa-hero, .ainative-hero, .teams-hero, .agentic-atmosphere > .photo-hero) .hero-copy :is(.svc-hero-copy, .genai-lede, .hero-lede) {
		margin-top: clamp(0.75rem, 8.333vh - 2.375rem, 1.375rem);
		max-width: 60ch; line-height: 1.6;
	}
	:is(.genai-hero, .cloud-hero, .bi-hero, .qa-hero, .ainative-hero, .teams-hero, .agentic-atmosphere > .photo-hero) .hero-copy .svc-hero-copy { font-size: clamp(1rem, 0.8vh + 0.7rem, 1.06rem); }
	:is(.genai-hero, .cloud-hero, .bi-hero, .qa-hero, .ainative-hero, .teams-hero, .agentic-atmosphere > .photo-hero) .hero-copy :is(.genai-lede, .hero-lede) { font-size: clamp(1rem, 1.6vh + 0.4rem, 1.12rem); }

	:is(.genai-hero, .cloud-hero, .bi-hero, .qa-hero, .ainative-hero, .teams-hero) .hero-copy .hero-cta { margin-top: clamp(1.125rem, 11.667vh - 3.25rem, 2rem); }
	.agentic-atmosphere > .photo-hero .hero-copy .hero-cta { margin-top: clamp(1.125rem, 13.333vh - 3.875rem, 2.125rem); }
	:is(.genai-hero, .cloud-hero, .bi-hero, .qa-hero, .ainative-hero, .teams-hero, .agentic-atmosphere > .photo-hero) .hero-cta .btn-lg {
		padding-top: clamp(0.75rem, 4.167vh - 0.8125rem, 1.0625rem);
		padding-bottom: clamp(0.75rem, 4.167vh - 0.8125rem, 1.0625rem);
	}
}
