/*
Theme Name: Skuridat
Theme URI: https://skuridat.com
Author: Skuridat
Description: Custom block theme for the Skuridat marketing site. Dark only. Eight views, a slab on a darker ground, and a dashed/solid hairline system that is the visual identity. No parent theme, no page builder, no form plugin.
Version: 0.11.68
Requires at least: 6.6
Tested up to: 6.8
Requires PHP: 8.2
License: GPL-2.0-or-later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
Text Domain: skuridat
Tags: full-site-editing, block-theme, dark
*/

/* ==========================================================================
   1. Tokens
   Colour, line and rhythm values come from design_handoff/README.md.
   Nothing below this file may hand-write a border or a hex value.
   ========================================================================== */

:root {
	/* --- Colour ------------------------------------------------------- */
	--ground:  #0f1e22;
	--page:    #172e34;
	--heading: #f2f2f0;
	--body:    #a8cbcf;
	--muted:   #5b6e74;
	--accent:  #fc561d;

	/* Text and marks use --accent; a filled surface uses --accent-fill. They
	   are the same colour until the AA contrast layer separates them: text
	   wants a lighter orange against the page, a button fill wants a darker
	   one under white. */
	--accent-fill: #fc561d;

	/* The button hover panel, on a button that is already filled. Derived
	   from --accent-fill rather than typed, so it is not a sixth hex to
	   keep in step and it follows the AA layer on its own. Mixed toward the
	   ground, not toward black, so it stays inside the palette's world.
	   White on it is 4.25:1, better than the 3.24:1 of the resting fill. */
	--accent-deep: color-mix(in srgb, var(--accent-fill) 84%, var(--ground));

	--hero-ground: #132428;

	/* --- Fills (all rgba so they compose over --page) ------------------ */
	--fill-card:    rgba(168, 203, 207, .05);
	--fill-band:    rgba(168, 203, 207, .07);
	--fill-form:    rgba(168, 203, 207, .08);
	--fill-input:   rgba(0, 0, 0, .22);
	--fill-table:   rgba(0, 0, 0, .18);
	--fill-diagram: rgba(168, 203, 207, .32);
	--fill-sep:     rgba(168, 203, 207, .35);
	--scrim:        rgba(10, 20, 23, .72);

	/* --- The line system. Dashed = a boundary or something unfilled.
	       Solid = a thing. Never normalise one into the other. ---------- */
	--line-section:  1.2px dashed rgba(168, 203, 207, .24);
	--line-slab:     1.2px solid  rgba(168, 203, 207, .24);
	--line-nav:      1.5px solid  rgba(168, 203, 207, .45);
	--line-box:      1.3px solid  rgba(168, 203, 207, .5);
	--line-soft:     1.3px dashed rgba(168, 203, 207, .3);
	--line-slot:     1.3px dashed rgba(168, 203, 207, .38);
	--line-emph:     1.3px solid  rgba(168, 203, 207, .6);
	--line-form:     1.5px solid  rgba(168, 203, 207, .55);
	--line-field:    1.3px solid  rgba(168, 203, 207, .4);
	--line-foot:     1.2px solid  rgba(168, 203, 207, .25);
	--line-legal:    1px   solid  rgba(168, 203, 207, .2);
	--line-table:    1px   solid  rgba(168, 203, 207, .16);
	--line-table-o:  1px   solid  rgba(168, 203, 207, .34);
	--line-menu:     1px   solid  rgba(168, 203, 207, .14);

	/* --- Type --------------------------------------------------------- */
	--font-display: "Archivo", system-ui, -apple-system, sans-serif;
	--font-body:    system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
	--font-mono:    ui-monospace, Menlo, Consolas, monospace;

	/* Fluid between the two designed breakpoints, 390 and 1440.
	   No third layout was designed; these interpolate the tablet range. */
	--fs-h2:      clamp(32px, 26.80px + 1.333vw, 46px);
	--fs-h2-cta:  clamp(32px, 24.57px + 1.905vw, 52px);
	--fs-h2-page: clamp(28px, 23.43px + 1.171vw, 36px);
	--fs-h3:      clamp(20px, 19.26px + 0.190vw, 22px);
	--fs-h3-lg:   clamp(22px, 18.29px + 0.952vw, 32px);
	--fs-h4:      clamp(14px, 13.63px + 0.095vw, 15px);
	--fs-body:    clamp(16px, 15.63px + 0.095vw, 17px);
	--fs-lead:    clamp(18px, 16.89px + 0.286vw, 21px);
	--fs-eyebrow: clamp(11px, 10.26px + 0.190vw, 13px);
	--fs-mark:    clamp(16px, 13.77px + 0.571vw, 22px);
	--fs-small:   clamp(13px, 12.63px + 0.095vw, 14px);

	--lh-body: 1.6;

	/* --- Rhythm ------------------------------------------------------- */
	--slab-max:   1440px;
	--gutter:     clamp(20px, 9.60px + 2.667vw, 48px);
	--nav-h:      clamp(56px, 47.09px + 2.286vw, 80px);
	/* The space between two menu labels, and (0.10.24) also between the
	   last label and the separator, and between the separator and the
	   language code. One value, so the three can never drift apart. */
	/* 32px from 1200px up, easing to 16px at 861px (the last width with the
	   full bar), so the labels and the button never wrap to two lines. */
	--nav-item-gap: clamp(16px, 4.72vw - 24.6px, 32px);
	/* How far the WordPress admin bar pushes the sticky top bar down. Zero
	   for every visitor; see section 31 for signed-in editors. */
	--bar-top:    0px;
	--sec-pad:    clamp(64px, 52.11px + 3.048vw, 96px);
	--cta-pad:    clamp(64px, 55.09px + 2.286vw, 88px);
	--card-pad:   clamp(20px, 15.54px + 1.143vw, 32px);

	/* The fused attribute panel's row padding, from wireframe 12d. Named
	   rather than typed inline so the panel's rhythm is adjustable in one
	   place, the way every other spacing value on this site is. */
	--panel-row-y: 26px;
	--panel-row-x: 30px;

	--gap-2col:   clamp(24px, 21.03px + 0.762vw, 32px);
	--gap-2wide:  clamp(24px, 9.14px + 3.810vw, 64px);
	--gap-3col:   clamp(12px, 7.54px + 1.143vw, 24px);
	--measure:    34em;

	--radius:     2px;
	--dur:        140ms;
	--ease:       ease-out;
}

/* ==========================================================================
   2. Reset and base
   ========================================================================== */

*, *::before, *::after { box-sizing: border-box; }

/* This site is dark only, and the browser has to be told. Without it every
   native control the CSS does not draw itself renders light: the consent
   checkbox was a white box on the dark form, and the scrollbars and the
   autofill highlight were light too. One declaration, and the UA draws the
   dark versions of all of them. */
:root { color-scheme: dark; }

html {
	-webkit-text-size-adjust: 100%;

	/* Every in-page jump lands under a bar that is now always there: the
	   contents rail, the phase nav, the skip link. One declaration on the
	   scroller offsets all of them, rather than a scroll-margin on each
	   thing that might ever be a target. */
	scroll-padding-top: calc(var(--bar-top) + var(--nav-h) + 24px);
}

/* Platform stacks a second sticky bar under the first, so a jump to a phase
   has two things to clear, not one. The rule above only knew about the top
   bar, which put the phase's own top edge 101px behind the phase band: the
   section looked glued to the bar because the boundary between them was
   hidden by it.

   `--phase-band-h` is published by theme.js, because the band's height is
   its content plus its padding and changes with the viewport; a number typed
   here would be right at 1440 and wrong everywhere else. With JavaScript off
   the variable is absent and the fallback reproduces the site-wide value, so
   the page behaves as it did before this rule existed.

   Nothing is added on top of the band's height, and that is the point. The
   jump lands the section's top edge exactly on the band's bottom edge, which
   is where the previous section's dashed divider sits. The two lines land on
   the same pixel row and read as one. Adding a gap here instead shows both of
   them, a few pixels apart, which reads as a mistake. The breathing room in
   front of the content is the section's own top padding, which is now fully
   visible rather than hidden behind the band. */
html:has(.phase-band--sticky) {
	scroll-padding-top: calc(var(--bar-top) + var(--nav-h) + var(--phase-band-h, 24px));
}

body {
	margin: 0;
	background: var(--ground);
	color: var(--body);
	font-family: var(--font-body);
	font-size: var(--fs-body);
	line-height: var(--lh-body);
	-webkit-font-smoothing: antialiased;
	text-rendering: optimizeLegibility;
}

body.is-locked { overflow: hidden; }

img, svg, video { max-width: 100%; height: auto; display: block; }

h1, h2, h3, h4, h5, h6, p, figure, blockquote, ul, ol, dl { margin: 0; }
ul, ol { padding: 0; list-style: none; }

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

button { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; }

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

.screen-reader-text {
	position: absolute;
	width: 1px; height: 1px;
	padding: 0; margin: -1px;
	overflow: hidden;
	clip-path: inset(50%);
	white-space: nowrap;
	border: 0;
}

.skip-link {
	position: absolute;
	left: var(--gutter); top: 8px;
	z-index: 200;
	padding: 10px 16px;
	background: var(--page);
	border: var(--line-form);
	color: var(--heading);
	font: 600 14px/1 var(--font-display);
	transform: translateY(-200%);
}
.skip-link:focus { transform: none; }

/* The skip link has to hand focus to something focusable or the browser moves
   the scroll and leaves focus on <body>, and the next Tab starts again from
   the wordmark. <main> takes tabindex="-1" for that, and draws no ring of its
   own: the point is where the next Tab goes, not a box round the whole page. */
.site-main:focus { outline: none; }

/* ==========================================================================
   3. The page slab
   The page does not fill the viewport. It is a bounded document on a darker
   ground, with a vertical hairline down each edge running the full height.
   "Full bleed" everywhere in this stylesheet means slab bleed, never
   viewport bleed and never gutter width.
   ========================================================================== */

.slab {
	max-width: var(--slab-max);
	margin-inline: auto;
	background: var(--page);
	border-left: var(--line-slab);
	border-right: var(--line-slab);
	min-height: 100vh;
	display: flow-root;
}

/* Below the slab width the edges sit flush against the viewport; below the
   tablet range they are dropped entirely, per the spec. */
@media (max-width: 1200px) {
	.slab { border-left: 0; border-right: 0; }
}

/* The gutter container. The slab edge and the gutter are two different
   boundaries. Do not collapse them. */
.wrap {
	max-width: var(--slab-max);
	margin-inline: auto;
	padding-inline: var(--gutter);
}

/* ==========================================================================
   4. The section wrapper
   Every section block renders inside this. It carries the dashed divider and
   the optional tint, both full bleed to the slab edge. No block below this
   line re-solves bleed.
   ========================================================================== */

.section {
	position: relative;
	padding-block: var(--sec-pad);
	border-bottom: var(--line-section);
}

/* The five Platform-page CTEM phase blocks (blocks/phase-section, ids
   phase-01..05): client feedback that the space above and below each
   phase's text-plus-illustration reads too generous. 20% less than the
   site's usual --sec-pad, scoped to just these five sections rather than
   changing --sec-pad itself, which every other section on the site also
   uses. */
.section[id^="phase-"] { padding-block: calc(var(--sec-pad) * .8); }

/* Client feedback, 28-09-2026, three more fixes to the same five phase
   blocks on mobile:

   Uniformity — .grid2 alternates textSide (left/right) to vary the desktop
   layout, but on mobile that same DOM order stacks each block differently:
   a "right" block (visual first) reads illustration-then-text, a "left"
   block (visual second) reads text-then-illustration, so scrolling past
   them the copy starts at a different height in every other section.
   .phase-section__copy/__visual (blocks/phase-section/render.php) now give
   every phase the same order regardless of textSide — copy always first —
   so the text lands at a consistent position going into each section.

   Margin — the mobile-wide `.grid2, .grid3 { gap: 12px }` override
   (section 2) reads too tight between a phase's text and its illustration
   once they're stacked instead of side by side. 24px for just these five
   sections, inside the 20-30px asked for. */
@media (max-width: 860px) {
	.section[id^="phase-"] .phase-section__copy { order: 1; }
	.section[id^="phase-"] .phase-section__visual { order: 2; min-width: 0; }
	.section[id^="phase-"] .grid2 { gap: 24px; }
}

.section--tint { background: var(--fill-band); }
.section--flush { padding-block: 0; }
.section--last { border-bottom: 0; }

/* Section washes fade out before the section does.

   A radial wash confined to its own section is still painting colour on the
   first and last row of that section, so where the section meets its
   neighbour there is a horizontal step. On a dark ground that step reads as
   a hard edge, as though the gradient had been given padding.

   The mask forces the wash to zero at both edges whatever the gradient
   inside it is doing. Retuning a radius or moving a centre later cannot
   bring the seam back, which is the reason this is a mask and not four
   hand-balanced gradient stops.

   Applies to the two washes on the site: the closing CTA and the home hero.
   It deliberately does NOT apply to .section--tint, the phase band or the
   footer. Those are flat fills, and a filled band ending at a boundary is
   the line system working, not a seam. */
.closing-cta__wash,
.hero__wash {
	--wash-fade: 24%;
	-webkit-mask-image: linear-gradient(to bottom,
		transparent 0,
		#000 var(--wash-fade),
		#000 calc(100% - var(--wash-fade)),
		transparent 100%);
	mask-image: linear-gradient(to bottom,
		transparent 0,
		#000 var(--wash-fade),
		#000 calc(100% - var(--wash-fade)),
		transparent 100%);
}

/* ==========================================================================
   5. Type
   ========================================================================== */

.eyebrow {
	font: 600 var(--fs-eyebrow) / 1.2 var(--font-display);
	letter-spacing: .18em;
	text-transform: uppercase;
	color: var(--accent);
	margin-bottom: 16px;
}
.eyebrow--body { color: var(--body); }

h1, .h1,
h2, .h2 {
	font: 600 var(--fs-h2) / 1.06 var(--font-display);
	letter-spacing: -.03em;
	color: var(--heading);
	max-width: 15em;
	margin-bottom: 24px;
}

.h2--cta {
	font-size: var(--fs-h2-cta);
	max-width: 11em;
}
.h2--page { font-size: var(--fs-h2-page); }

/* The .h3 and .h4 classes carry the look independently of the tag, so a
   heading can sit at the level the document outline needs while looking the
   way the design drew it. They reset max-width and margin too, because the
   h1/h2 rule above sets both and a class that only changed the font would
   leave a promoted heading wearing half of an h2. */
h3, .h3 {
	font: 600 var(--fs-h3) / 1.2 var(--font-display);
	letter-spacing: -.02em;
	color: var(--heading);
	max-width: none;
	margin-bottom: 0;
}
.h3--lg { font-size: var(--fs-h3-lg); }

/* phase-section's heading is an h2 styled as h3--lg; three-col-detail's h3--lg
   card labels are h3, so this only reaches phase-section. Give it the same
   24px gap before its body copy that the page-level h2/h2--page headings
   (three-col-detail, prose pages) already have — confirmed live on both
   /platform/ (was 0px) and /services/ (24px) before this change. */
h2.h3--lg { margin-bottom: 24px; }

h4, .h4 {
	font: 600 var(--fs-h4) / 1.3 var(--font-display);
	letter-spacing: -.01em;
	color: var(--heading);
	max-width: none;
	margin-bottom: 0;
}

p { max-width: var(--measure); }
p + p { margin-top: 16px; }

.lead {
	font-size: var(--fs-lead);
	max-width: 30em;
	color: var(--body);
}

.caption, .legal {
	font-size: var(--fs-small);
	color: var(--muted);
	max-width: 40em;
}

.mono-label {
	font: 600 12px/1.2 var(--font-mono);
	letter-spacing: .1em;
	text-transform: uppercase;
	color: var(--muted);
}

.fineprint {
	font-size: var(--fs-small);
	color: var(--muted);
	max-width: 40em;
}

.talk-first {
	font: 400 14px/1.4 var(--font-body);
	color: var(--body);
	max-width: none;
}
.talk-first a { color: var(--accent); }
.talk-first a:hover { filter: brightness(1.15); }

.pull-quote {
	font: 500 clamp(20px, 17.77px + 0.571vw, 26px) / 1.35 var(--font-display);
	color: var(--heading);
	max-width: 20em;
	border-left: 2px solid var(--accent);
	padding: clamp(16px, 10.06px + 1.524vw, 32px);
	margin-block: 32px;
}

/* ==========================================================================
   6. Buttons
   No lift, no shadow. Accent does not change hue on hover.
   ========================================================================== */

.btn {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	gap: 8px;
	font: 600 16px/1 var(--font-display);
	padding: 16px 28px;
	border-radius: var(--radius);
	background: var(--accent-fill);
	color: #fff;
	border: 1.3px solid var(--accent-fill);
	transition: border-color var(--dur) var(--ease), color var(--dur) var(--ease);
	cursor: pointer;
	text-align: center;

	/* The hover panel below needs somewhere to sit and something to clip
	   it. isolation makes the button its own stacking context, so the panel
	   can go to z-index -1 and land above the button's background but under
	   its label, without the label having to be wrapped in a span. That is
	   what keeps this a stylesheet change and not a template one.

	   overflow does not clip the focus outline: outlines are not clipped. */
	position: relative;
	overflow: hidden;
	isolation: isolate;
}

/* A slanted panel parked off the left edge, sliding across on hover. It
   replaces the brightness bump the buttons used to do, which was a filter on
   the whole element and could not say anything about direction.

   It travels left to right, with the page. The skew is unchanged, so the
   leading edge is the panel's right edge and the top of it arrives first.

   Wider than the button (inset -25% each side) because skewing a box the same
   width as its container pulls the corners inside it. The global
   prefers-reduced-motion rule covers this transition: its selector includes
   *::before. */
.btn::before {
	content: "";
	position: absolute;
	inset: -2px -25%;
	z-index: -1;
	background: var(--accent-deep);
	transform: skewX(-14deg) translateX(-112%);
	transition: transform 380ms cubic-bezier(.21, .6, .35, 1);
}
.btn:hover::before,
.btn:focus-visible::before { transform: skewX(-14deg) translateX(0); }

/* Briefly --heading (white) at rest for one 0.11.01 build; reverted the
   same day — on reflection the grey resting label read better, and hover
   already goes white, so that state change still does the same job it
   always did. */
.btn--ghost {
	background: transparent;
	color: var(--body);
	border: var(--line-field);
}

/* The ghost button is where this does real work: outline to filled is a
   state change, not a shade. The panel takes --accent-fill rather than
   --accent, because the label goes white on it and --accent-fill is the
   value the contrast layer measures for exactly that pairing. */
.btn--ghost::before { background: var(--accent-fill); }
.btn--ghost:hover,
.btn--ghost:focus-visible { color: #fff; border-color: var(--accent-fill); }

/* The optional AA contrast layer. Off unless Settings > Skuridat switches it
   on. Four tokens, nothing else: see inc/accessibility.php for the measured
   ratios and the reasoning. */
/* Measured against every ground the site actually puts these colours on,
   which was found by walking the rendered pages rather than assumed: the hero
   #132428, the artefact table #13262b, the page #172e34, a tinted band
   #21393f, and a form panel #233b40.

   One ground is deliberately left out. The gated-paper form sits inside a
   tinted band, so its own fill composes twice to #2c454a, and tuning the
   accent for that one spot cost most of the brand orange: #fd8f69 rather than
   #fd7445. The only accent text down there is a single "Privacy & data
   handling" link, so that link takes the heading colour instead, keeping its
   underline. One link handled on its own, and the orange stays recognisable
   everywhere else. */
body.aa-contrast {
	--muted:       #8ea1a6;   /* 2.66:1 -> 4.53:1 on the worst ground it sits on */
	--accent:      #fd7445;   /* 4.39:1 -> 4.51:1 on the worst ground it sits on */
	--accent-fill: #dc3a03;   /* white label 3.24:1 -> 4.52:1 */
	--line-field:  1.3px solid rgba(168, 203, 207, .48);  /* 2.56:1 -> 3.05:1 */
}

/* The one exception, above. Inside a form panel the accent cannot reach 4.5
   without going pale, and these two links are already marked by an underline,
   so colour is not carrying the job on its own. */
body.aa-contrast .field-consent a,
body.aa-contrast .modal__scope .caption a { color: var(--heading); }

.btn--sm { font-size: 14px; padding: 12px 20px; }

.btn--block { width: 100%; }

@media (max-width: 860px) {
	.btn { width: 100%; height: 48px; padding-block: 0; font-size: 15px; }
	.btn--sm { width: auto; height: auto; }
}

/* ==========================================================================
   7. Boxes, cards and placeholder slots
   Solid = a thing. Dashed = a boundary or something not yet filled.
   ========================================================================== */

.box {
	background: var(--fill-card);
	border: var(--line-box);
	border-radius: var(--radius);
	padding: var(--card-pad);
}

.box--soft {
	background: none;
	border: var(--line-soft);
}

.box--emph { border: var(--line-emph); }

.box h3, .box .h3 { margin-bottom: 12px; }

/* The designed treatment for an unfilled image slot. This is not a stand-in
   for something better: it is what an empty slot looks like on this site,
   and it keeps its label so you can see what it is waiting for. */
.slot {
	display: flex;
	align-items: center;
	justify-content: center;
	text-align: center;
	padding: 16px;
	border: var(--line-slot);
	border-radius: var(--radius);
	background-image: repeating-linear-gradient(135deg,
		rgba(168, 203, 207, .13) 0 8px, transparent 8px 16px);
	font: 400 14px/1.4 var(--font-mono);
	color: var(--body);
}

@media (max-width: 860px) {
	.slot { font: 400 12px/1.4 var(--font-body); color: var(--muted); }
}

/* Three slot sizes. Do not introduce a fourth. */
.slot--feature { height: 480px; }
.slot--inline  { height: 320px; }
.slot--thumb   { height: 180px; }

@media (max-width: 860px) {
	.slot--feature { height: 240px; }
	.slot--inline  { height: 200px; }
	.slot--thumb   { height: 96px; }
}

.slot--pill { height: 56px; padding-inline: 12px; }
.slot--chip { width: 72px; height: 72px; padding: 0; }
/* Kept for the harness and for any slot still labelled "social"; the real
   rows are .social below. */
.slot--social { width: 36px; height: 36px; padding: 0; font-size: 12px; }
.slot--cred { height: 60px; padding-inline: 12px; font-size: 12px; }
/* The image counterpart to .slot--cred (0.11.32): a real badge, uploaded
   per founder, shown at a fixed square footprint like the ticker's own
   logos — cover, not contain, since the badges are already square.
   60px (0.11.33, was 40px, client feedback the badges read too small) —
   kept in step with .slot--cred's own height above it so an image badge
   and a still-text fallback pill line up in the same row. */
.media--cred { width: 60px; height: 60px; flex: none; }
.media--cred .media__img { width: 100%; height: 100%; object-fit: cover; border-radius: var(--radius); }

/* ==========================================================================
   8. Grids
   ========================================================================== */

.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: var(--gap-2col); }
.grid2--wide { gap: var(--gap-2wide); }
.grid3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--gap-3col); }
.row { display: flex; align-items: center; gap: 20px; flex-wrap: wrap; }

@media (max-width: 860px) {
	.grid2, .grid3 { grid-template-columns: 1fr; gap: 12px; }
	.row { gap: 12px; }
}

/* ==========================================================================
   9. Top bar
   The nav rule is the heaviest line on the page. It sits on the full-bleed
   band, slab edge to slab edge, never on the gutter container.
   ========================================================================== */

/* The bar follows the page down.

   Sticky goes on the <header>, not on .navband inside it. A sticky element
   travels within its containing block, and the header is exactly as tall as
   the bar, so sticky set on .navband would have nowhere to go and would sit
   there looking like position: relative.

   The background is the flat --page, not a tint and not a blur. Anything
   translucent lets the sections scroll *through* the bar rather than behind
   it, which reads as broken. The phase nav settled the same argument in
   section 18; this is the same rule applied to the shell.

   z-index 60 sits under the mobile sheet (90) and the modal (120), and over
   the sticky phase nav (50), which is what should slide under it. */
.slab > header {
	position: sticky;
	top: var(--bar-top);
	z-index: 60;
}

.navband {
	border-bottom: var(--line-nav);
	background: var(--page);
	position: relative;
}

.nav {
	display: flex;
	align-items: center;
	height: var(--nav-h);
	gap: 12px;
}

.wordmark { display: inline-flex; }
.wordmark__img {
	display: block;
	/* Was 1.5x --fs-mark (24-09-2026); doubled to 3x on 25-09-2026 when
	   the client's 834x290 wordmark read too small at the old scale.
	   Reverted to 1.5x on 27-09-2026 along with the wordmark and favicon
	   files themselves (back to the plain "SKURIDAT" wordmark and the "K"
	   mark, both from 24-09-2026) — the 3x scale was tuned for that one
	   file's own proportions, not a fixed brand size, so it goes back to
	   1.5x with the file it was originally tuned for.
	   Client call, 28-09-2026: still read too large at 1.5x; asked for a
	   fixed 205px width rather than another scale of --fs-mark, so this
	   is now a plain fixed size, not part of the responsive type clamp. */
	width: 205px;
	height: auto;
}
/* width: auto here matters, not just the height change: the header rule
   above sets a fixed 205px width, and without overriding it back to auto
   this box was 205x20 while the file itself is 1000x138 — stretched to a
   10.25:1 box against its own 7.25:1, visibly distorted. Caught while
   sizing the footer mark down to "ongeveer 75%" (client call,
   29-09-2026): 20px * .75 = 15px, width auto so it scales proportionally
   instead of stretching. */
.wordmark--foot .wordmark__img { width: auto; height: 15px; }

/* Client call, 29-09-2026: line the logo up with the "Site"/"Company"/
   "Credentials" column heads beside it. `.wordmark`'s own `inline-flex`
   is right for the header, sitting next to the nav links, but here it's
   the top of its own column, alone — and an inline-level box still sits
   inside an anonymous line box, which left a ~5px phantom gap above the
   15px logo that a block-level `<p class="footer-head">` never had.
   `display: flex` (block-level, same internal centering) drops it out of
   that line box; confirmed live, both now sit on the exact same pixel. */
.wordmark--foot { display: flex; }

/* 205px reads right beside the desktop nav links, but on its own against a
   375px screen — the client asked about the header in general, not this
   width specifically — it would out-size the hamburger button next to it.
   Scaled down at the same mobile breakpoint the nav itself collapses at. */
@media (max-width: 860px) {
	.wordmark__img { width: 150px; }
}

.nav__links {
	display: flex;
	align-items: center;
	gap: var(--nav-item-gap);
	margin-left: auto;
}
.nav__links a {
	white-space: nowrap;
	font: 400 15px/1 var(--font-body);
	color: var(--body);
	padding-bottom: 3px;
	border-bottom: 2px solid transparent;
	transition: color var(--dur) var(--ease), border-color var(--dur) var(--ease);
}

/* Hover draws the same bar the current page has, in the colour the label
   turns. currentColor, so the two can never be set to different values by
   accident. The current page keeps its accent bar on hover: that rule comes
   after this one at the same specificity, so it wins. */
.nav__links a:hover,
.nav__links a:focus-visible { color: var(--heading); border-bottom-color: currentColor; }

.nav__links a[aria-current="page"] { color: var(--heading); border-bottom-color: var(--accent); }

.nav .btn--sm { margin-left: 24px; white-space: nowrap; }

/* Language switcher. A two-letter locale code, not a drawn flag: a CSS flag
   does not survive translation and flags are a poor proxy for language. */
.lang {
	position: relative;
}
.lang__toggle {
	display: flex;
	align-items: center;
	gap: 6px;
	font: 400 15px/1 var(--font-body);
	text-transform: uppercase;
	color: var(--body);
	padding: 0 4px 3px;
	border: none;
	border-bottom: 2px solid transparent;
	background: none;
}
.lang__toggle span { color: var(--muted); font-size: 10px; }
.lang__toggle:hover span,
.lang__toggle:focus-visible span { color: var(--accent); }
/* The rule between the menu links and the language switcher. It is drawn as
   an element rather than a border, so its colour is a fill token and not one
   of the --line-* border shorthands. */
.nav__sep {
	width: 1px;
	height: 20px;
	background: var(--fill-sep);
	/* The bar's flex gap is 12px, so each side adds the rest of the menu's
	   item gap. On the right, 4px less again: the language toggle has 4px of
	   its own padding before the letters. Measured label to rule and rule to
	   label, both are exactly --nav-item-gap. */
	margin-left: calc(var(--nav-item-gap) - 12px);
	margin-right: calc(var(--nav-item-gap) - 12px - 4px);
	flex: none;
}
.lang__list {
	position: absolute;
	top: calc(100% + 6px);
	left: 0;
	/* Sized for two-letter codes since 0.10.26, not full names. */
	min-width: 72px;
	background: var(--page);
	border: var(--line-field);
	border-radius: var(--radius);
	padding: 4px;
	z-index: 70;
}
.lang__list a {
	display: block;
	padding: 8px 12px;
	font-size: 14px;
	color: var(--body);
}
.lang__list a:hover { color: var(--heading); }
/* The language you are already in. Colour on its own would make this a
   colour-only signal, so it takes the accent bar the nav items and the
   filter pills use, turned on its side. */
.lang__list a[aria-current] {
	color: var(--heading);
	box-shadow: inset 2px 0 0 var(--accent);
}
.lang__list[hidden] { display: none; }

/* Mobile bar: current page name, burger, no language switcher. */
.nav__current {
	margin-left: auto;
	font: 400 14px/1 var(--font-body);
	color: var(--heading);
	border-bottom: 2px solid var(--accent);
	padding-bottom: 3px;
}

/* The drawn mark stays 22x16, which is what the design shows. The tappable
   area is 44x44, because this is the only navigation control on a phone and
   22x16 is a poor target for a thumb. WCAG 2.2's 2.5.8 lets it through on the
   spacing exception (the nearest other target is 288px away), which is a
   reason not to have to fix it, not a reason to leave it. */
.burger {
	display: none;
	width: 44px;
	height: 44px;
	padding: 14px 11px;
	margin-right: -11px;
	flex-direction: column;
	justify-content: space-between;
	/* Pushes the burger to the bar's right edge on its own (the landing page
	   prints no .nav__current, so it is otherwise the last visible flex
	   child with nothing left to push against it — that stranded it next to
	   the wordmark on the left). Where .nav__current does exist, its own
	   margin-left: auto already pulls the pair right, so the sibling rule
	   below restores the small gap between it and the burger instead. */
	margin-left: auto;
}
.nav__current + .burger { margin-left: 16px; }
.burger i {
	display: block;
	width: 22px;
	height: 1.6px;
	background: var(--heading);
}

@media (max-width: 860px) {
	.nav__links, .nav .btn--sm, .nav .lang, .nav__sep { display: none; }
	.burger { display: flex; }
}
@media (min-width: 861px) {
	.nav__current { display: none; }
}

/* ==========================================================================
   10. Mobile menu
   A full-viewport sheet that replaces the page, not an overlay.
   ========================================================================== */

.mobile-menu {
	position: fixed;
	inset: calc(var(--bar-top) + var(--nav-h)) 0 0;
	z-index: 90;
	background: var(--page);
	padding: 32px var(--gutter) 32px;
	display: flex;
	flex-direction: column;
	overflow-y: auto;
}
.mobile-menu[hidden] { display: none; }

.mobile-menu__links a {
	display: block;
	font: 500 24px/1.2 var(--font-display);
	letter-spacing: -.02em;
	color: var(--body);
	padding-block: 16px;
	border-bottom: var(--line-menu);
}
/* The same move as the top bar: hover turns the label white and draws the
   bar in the colour the label turned. The rule for the current page comes
   after it, so the accent bar survives being hovered. */
.mobile-menu__links a:hover,
.mobile-menu__links a:focus-visible {
	color: var(--heading);
	border-bottom: 2px solid currentColor;
}

.mobile-menu__links a[aria-current="page"] {
	color: var(--heading);
	border-bottom: 2px solid var(--accent);
}

.mobile-menu__bottom { margin-top: auto; padding-top: 32px; display: grid; gap: 20px; }
.mobile-menu__lang { display: flex; align-items: center; gap: 10px; font-size: 15px; color: var(--heading); }
.mobile-menu__util { display: flex; gap: 16px; font-size: 14px; color: var(--muted); }

/* The dropdown triangle reads small at 10px next to the rest of the mobile
   sheet's type; a bit bigger here, capped well under 2x per the brief. */
.mobile-menu__lang .lang__toggle span { font-size: 14px; }

@media (min-width: 861px) {
	.mobile-menu { display: none !important; }
}

/* ==========================================================================
   11. Footer
   The fill and the top rule bleed to the slab edge. The legal baseline is
   the one deliberate exception: it is inset to the gutter.
   ========================================================================== */

.site-footer {
	background: var(--fill-input);
	border-top: var(--line-foot);
	padding-top: 80px;
	padding-bottom: 32px;
}

.site-footer__grid {
	display: grid;
	grid-template-columns: 1.4fr 1fr 1fr 1.1fr;
	gap: 40px;
}

.site-footer p { font-size: 15px; }

.footer-head {
	font: 600 12px/1 var(--font-display);
	letter-spacing: .14em;
	text-transform: uppercase;
	color: var(--heading);
	margin-bottom: 16px;
}

.site-footer__tagline { margin-top: 8px; max-width: 22em; }

/* Site and Company share a wrapper so the mobile 2-up grid has something to
   grab. On desktop the wrapper dissolves into the 4-column grid. */
.site-footer__nav { display: contents; }

.site-footer__links a { display: block; line-height: 2; color: var(--body); font-size: 15px; }
.site-footer__links a:hover { color: var(--heading); }

/* Shown in their own colours, full strength — client call, 24-09-2026:
   real confirmed badges read as confirmations, not as another muted line
   in the footer. (Was filter: grayscale(1); opacity: .7 while this row
   still mixed real badges with unconfirmed text placeholders.) 1.3x the
   .cred-pill-image base size (40px), scoped to the footer only — the
   credential-pills block isn't used anywhere yet, and its own default
   size shouldn't change just because the footer's badges grew. */
.site-footer__creds { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; }
.site-footer__creds .cred-pill-image { height: calc(40px * 1.3); }

.site-footer__legal {
	display: flex;
	justify-content: space-between;
	gap: 16px;
	margin-top: 48px;
	padding-top: 22px;
	border-top: var(--line-legal);
	font: 400 13px/1.4 var(--font-body);
	color: var(--muted);
}

@media (max-width: 860px) {
	.site-footer { padding-top: 40px; padding-bottom: 28px; background: rgba(0, 0, 0, .28); }
	.site-footer__grid { grid-template-columns: 1fr; gap: 32px; }
	.site-footer__nav { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; }
	.site-footer__creds { display: flex; flex-wrap: wrap; gap: 14px; }
	.site-footer__legal { flex-direction: column; gap: 6px; margin-top: 32px; font-size: 12px; }
}

/* ==========================================================================
   12. Motion
   ========================================================================== */

@media (prefers-reduced-motion: reduce) {
	*, *::before, *::after {
		animation-duration: .01ms !important;
		animation-iteration-count: 1 !important;
		transition-duration: .01ms !important;
		scroll-behavior: auto !important;
	}
}

/* ==========================================================================
   13. Closing CTA
   Full bleed, two radial washes behind, an artefact table on the right.
   The same component on all eight views.
   ========================================================================== */

.closing-cta {
	position: relative;
	overflow: hidden;
}

.closing-cta__wash {
	position: absolute;
	inset: 0;
	background:
		radial-gradient(60% 90% at 12% 50%, rgba(252, 86, 29, .10), transparent 60%),
		radial-gradient(72% 100% at 88% 45%, rgba(168, 203, 207, .26), transparent 62%);
	pointer-events: none;
}

.closing-cta__grid {
	position: relative;
	display: grid;
	grid-template-columns: 1fr 480px;
	gap: clamp(32px, 17.14px + 3.81vw, 72px);
	align-items: center;
	padding: var(--cta-pad) var(--gutter);
	max-width: var(--slab-max);
	margin-inline: auto;
}

.closing-cta__actions { margin-top: 40px; gap: 20px; }
.closing-cta .fineprint { margin-top: 24px; }

.artefact__caption { margin-bottom: 14px; letter-spacing: .12em; font-weight: 500; }

.artefact {
	border: var(--line-table-o);
	border-radius: var(--radius);
	background: var(--fill-table);
}

.artefact__row {
	display: flex;
	justify-content: space-between;
	align-items: center;
	gap: 16px;
	padding: 13px 18px;
	border-top: var(--line-table);
	font: 400 14px/1.3 var(--font-mono);
	color: var(--body);
}
.artefact__row:first-child { border-top: 0; }

.artefact__row--head {
	font: 600 12px/1.2 var(--font-mono);
	letter-spacing: .1em;
	text-transform: uppercase;
	color: var(--muted);
	padding-block: 14px;
}

.artefact__row.is-accent { color: var(--accent); }
.artefact__row.is-muted  { color: var(--muted); }
.artefact__state { letter-spacing: .04em; text-align: right; }

.artefact__disclaimer { margin-top: 14px; }

@media (max-width: 980px) {
	.closing-cta__grid { grid-template-columns: 1fr; }
}

@media (max-width: 860px) {
	.closing-cta__actions { flex-direction: column; align-items: stretch; gap: 16px; }
	.talk-first--centred, .closing-cta__actions .talk-first { text-align: center; }

	/* Rows become two lines: the name on top, the state beneath. */
	.artefact__row {
		display: block;
		color: var(--heading);
	}
	.artefact__row.is-accent { color: var(--accent); }
	.artefact__state {
		display: block;
		text-align: left;
		margin-top: 4px;
		font-size: 13px;
		color: var(--muted);
	}
	.artefact__row--head { display: flex; }
}

/* ==========================================================================
   14. The request modal
   Desktop dialog, mobile sheet. Same markup, same form.
   ========================================================================== */

.modal {
	position: fixed;
	inset: 0;
	z-index: 120;
	display: flex;
	align-items: center;
	justify-content: center;
	padding: 24px;
}
.modal[hidden] { display: none; }

.modal__scrim {
	position: absolute;
	inset: 0;
	background: var(--scrim);
}

.modal__dialog {
	position: relative;
	width: min(1040px, 100%);
	max-height: calc(100vh - 48px);
	overflow-y: auto;
	background: var(--page);
	border: var(--line-form);
	border-radius: var(--radius);
	box-shadow: 0 32px 80px rgba(0, 0, 0, .55);
}

.modal__head {
	display: flex;
	align-items: flex-start;
	justify-content: space-between;
	gap: 24px;
	padding: 28px 40px;
	border-bottom: var(--line-table);
}
.modal__head .eyebrow { margin-bottom: 6px; }
.modal__subline { font-size: 15px; color: var(--body); }

.modal__close {
	font-size: 24px;
	line-height: 1;
	color: var(--body);
	padding: 4px 8px;
	transition: color var(--dur) var(--ease);
}
.modal__close:hover { color: var(--heading); }

.modal__body {
	display: grid;
	grid-template-columns: 1fr 1fr;
	gap: 48px;
	padding: 40px;
}

.modal__scope { display: grid; gap: 24px; align-content: start; }
.modal__scope-mobile { display: none; }
/* Neither rule above accounts for [hidden] — an author rule always beats
   the UA stylesheet's default `[hidden] { display: none }`, tie or no tie,
   so setting the attribute alone (patterns/request-modal.php, theme.js)
   had no visual effect until these existed. Same shape as the .modal
   [hidden]/:target fix elsewhere in this file, on a different element. */
.modal__scope[hidden],
.modal__scope-mobile[hidden] { display: none; }

.scope-item { display: grid; grid-template-columns: 32px 1fr; gap: 16px; }
.scope-item__n {
	font: 400 14px/1.4 var(--font-mono);
	color: var(--accent);
}
.scope-item h4 { margin-bottom: 6px; }
.scope-item p { font-size: 15px; }

/* --- Form --------------------------------------------------------------- */

.form-panel {
	background: var(--fill-form);
	border: var(--line-form);
	border-radius: 3px;
	padding: 32px;
	display: grid;
	gap: 18px;
	align-content: start;
}

.field input,
.field textarea {
	width: 100%;
	background: var(--fill-input);
	border: var(--line-field);
	border-radius: var(--radius);
	color: var(--heading);
	font: 400 15px/1.4 var(--font-body);
	padding: 0 16px;
	height: 48px;
	transition: border-color var(--dur) var(--ease);
}
.field textarea { height: 84px; padding: 14px 16px; resize: vertical; }

/* The callback modal's note field only (0.11.21): two rows initially —
   14px padding top/bottom plus two lines at this font's 21px line-height
   — with the same drag handle as every other textarea for anyone who
   needs more room. Scoped to this one field, not `.field textarea`
   itself, so the request modal's own (unrelated) message field keeps its
   existing height. */
.field textarea.textarea--sm { height: 70px; }

/* Placeholder as label. The asterisks are muted, deliberately, not accent. */
.field input::placeholder,
.field textarea::placeholder { color: var(--muted); opacity: 1; }

.field input:hover,
.field textarea:hover { border-color: rgba(168, 203, 207, .55); }

.field input[aria-invalid="true"],
.field textarea[aria-invalid="true"] { border-color: var(--accent); }

.field-honey {
	position: absolute;
	left: -9999px;
	width: 1px;
	height: 1px;
	overflow: hidden;
}

/* Body colour, not muted. This sentence is what someone is agreeing to when
   they tick the box, so it should not be the faintest text on the page. It is
   also the only text on the site sitting on a form panel inside a tinted
   band, where muted grey reads at 3.8:1 however it is tuned. */
.field-consent {
	display: grid;
	grid-template-columns: 18px 1fr;
	gap: 12px;
	align-items: start;
	font-size: 13px;
	color: var(--body);
}
.field-consent input { margin-top: 3px; accent-color: var(--accent); }
.field-consent a { color: var(--accent); }

.form-error {
	font-size: 14px;
	color: var(--accent);
	margin: 0;
}
.form-error[hidden] { display: none; }

.form-success {
	grid-column: 1 / -1;
	border: var(--line-form);
	background: var(--fill-form);
	border-radius: 3px;
	padding: 32px;
	color: var(--heading);
}
.form-success[hidden] { display: none; }

.form-panel .btn[disabled] { opacity: .6; cursor: progress; }

@media (max-width: 980px) {
	.modal__body { grid-template-columns: 1fr; gap: 24px; }
}

@media (max-width: 860px) {
	/* A full-screen sheet, not a dialog. The top bar stays visible above it. */
	.modal { padding: 0; align-items: flex-end; }
	.modal__dialog {
		position: fixed;
		inset: calc(var(--bar-top) + var(--nav-h)) 0 0;
		width: 100%;
		max-height: none;
		border-radius: 12px 12px 0 0;
		border-left: 0;
		border-right: 0;
		border-bottom: 0;
		box-shadow: none;
	}
	.modal__head { padding: 20px var(--gutter); }
	.modal__body { padding: 20px var(--gutter) 40px; }
	.modal__scope { display: none; }
	.modal__scope-mobile { display: block; }
	.form-panel { padding: 0; background: none; border: 0; }
}

/* ==========================================================================
   15. Prose
   The article measure. Used by the prose block and the utility pages.
   ========================================================================== */

.prose { max-width: var(--measure); }

/* A policy or a privacy statement is a long document with real structure,
   so prose gets both heading levels. h2 at the page-heading size rather
   than the section size: 46px section headings inside a legal document
   read as shouting. */
.prose h2 {
	font-size: var(--fs-h2-page);
	max-width: none;
	margin-top: 56px;
	margin-bottom: 16px;
}
.prose > h2:first-child { margin-top: 0; }
.prose h3 { margin-top: 40px; margin-bottom: 12px; }
.prose h4 { margin-top: 28px; margin-bottom: 8px; }
.prose ol { margin: 16px 0; padding-left: 20px; list-style: decimal; }
/* --- Tables in prose ------------------------------------------------------

   The same object as the artefact table in the closing CTA, and deliberately
   so: outer edge at --line-table-o, inner lines at --line-table, the darker
   --fill-table ground, and a mono uppercase header. A table is a thing, so
   every line here is solid.

   The outer edge lives on the <figure>, not on the <table>. With
   border-collapse the cell borders and the table's own border collapse into
   one, and the cell wins on the element order, so an outer weight set on the
   table simply disappears. On the figure it cannot be collapsed away.

   The selectors carry an extra element on purpose. WordPress ships its own
   table block CSS at .wp-block-table th, which is the same specificity as a
   bare .prose th, so which one wins comes down to load order. .prose table th
   is one step higher and settles it. */

.prose figure.wp-block-table,
.prose > table {
	margin: 28px 0;
	border: var(--line-table-o);
	border-radius: var(--radius);
	background: var(--fill-table);
	/* Three columns will not fit a phone. The table scrolls inside its own
	   box rather than pushing the page sideways. */
	overflow-x: auto;
}

.prose figure.wp-block-table > table { margin: 0; border: 0; }

.prose table {
	width: 100%;
	border-collapse: collapse;
	font-size: 15px;
}

.prose table th,
.prose table td {
	text-align: left;
	padding: 13px 18px;
	vertical-align: top;
	border-top: var(--line-table);
	border-left: var(--line-table);
	border-right: 0;
	border-bottom: 0;
	color: var(--body);
}

/* The outer edge is the figure's. Cells only draw the grid inside it. */
.prose table tr > :first-child { border-left: 0; }
.prose table tbody tr:first-child > * { border-top: 0; }

.prose table thead th {
	border-top: 0;
	/* The header is a different kind of row, so its lower edge takes the
	   outer weight rather than the inner one. */
	border-bottom: var(--line-table-o);
	font: 600 12px/1.2 var(--font-mono);
	letter-spacing: .1em;
	text-transform: uppercase;
	color: var(--muted);
	white-space: nowrap;
}

/* A caption under the table reads as a caption, not as another row. */
.prose figure.wp-block-table figcaption {
	margin: 0;
	padding: 12px 18px;
	border-top: var(--line-table);
	font-size: var(--fs-small);
	color: var(--muted);
}

@media (max-width: 600px) {
	.prose table th,
	.prose table td { padding: 11px 14px; }
	.prose table { font-size: 14px; }
}
.prose code { font: 400 .92em var(--font-mono); color: var(--heading); }
.prose p + p { margin-top: 16px; }
.prose ul { margin: 16px 0; padding-left: 20px; list-style: disc; }
.prose li { margin-bottom: 8px; }
/* A link in running text is orange. A button is not a link in running text:
   unscoped, this rule painted the Contact page's only call to action orange
   on orange, and it had been invisible since the page went up. */
.prose a:not(.btn) { color: var(--accent); }

/* ==========================================================================
   Links inside running text
   Orange against #a8cbcf body copy is 1.86:1 of colour difference, which is
   under the 3:1 WCAG asks for when colour is the only thing marking a link.
   An underline is the other thing. It is scoped to prose and to the few
   sentences elsewhere that carry an inline link: nav items, cards, pills and
   buttons are marked by shape and are deliberately left alone.
   ========================================================================== */

.prose a:not(.btn),
.article-body a:not(.btn),
.field-consent a,
.talk-first a,
.modal__scope .caption a,
.site-footer__legal a {
	text-decoration: underline;
	text-decoration-thickness: 1px;
	text-underline-offset: .18em;
}

/* Utility that a few components share. */
.talk-first--centred { text-align: center; margin-inline: auto; }

/* ==========================================================================
   16. Hero
   Static. Both washes at full opacity, because the rest state of the
   designed mechanic is "both sides visible".
   ========================================================================== */

.hero__stage {
	position: relative;
	overflow: hidden;
	background: var(--hero-ground);
}

.hero__wash { position: absolute; inset: 0; pointer-events: none; }
.hero__wash--left  { background: radial-gradient(70% 78% at 0% 34%, rgba(252, 86, 29, .22), transparent 62%); }
.hero__wash--right { background: radial-gradient(70% 78% at 100% 34%, rgba(168, 203, 207, .24), transparent 62%); }

.hero__cols {
	position: relative;
	display: grid;
	grid-template-columns: 36% 1fr 36%;
	align-items: center;
	min-height: 520px;
	padding: 72px 56px;
	gap: 24px;
}

.hero__col { display: flex; flex-direction: column; gap: 14px; }
.hero__col--left { align-items: flex-start; }
.hero__col--right { align-items: flex-end; text-align: right; }

.hero__tag {
	border: var(--line-slot);
	border-radius: var(--radius);
	padding: 10px 16px;
	font-size: 13px;
	color: var(--body);
	background-image: repeating-linear-gradient(135deg,
		rgba(168, 203, 207, .13) 0 8px, transparent 8px 16px);
}
.hero__tag--attack { border-color: rgba(252, 86, 29, .55); color: var(--accent); }

.hero__question { max-width: 14em; font-size: 16px; margin-top: 8px; }
.hero__question--attack { color: var(--accent); }

.hero__figure { display: flex; justify-content: center; }
.hero__figure-slot { width: 210px; height: 320px; }

.hero__headline { position: relative; text-align: center; padding-bottom: 88px; padding-top: 8px; }
.hero__headline h1 { margin: 0 auto 16px; max-width: 20em; }
.hero__headline p { margin: 0 auto 24px; max-width: 38em; }
.hero__actions { justify-content: center; }

@media (max-width: 980px) {
	.hero__cols {
		grid-template-columns: 1fr;
		min-height: 0;
		padding: 40px var(--gutter);
		gap: 20px;
	}
	.hero__figure { order: 2; }
	.hero__col--right { order: 3; }
	.hero__tag--extra { display: none; }
	.hero__figure-slot { width: 150px; height: 210px; }
	.hero__headline { text-align: left; padding-bottom: 48px; }
	.hero__headline h1, .hero__headline p { margin-inline: 0; }
	.hero__actions { justify-content: stretch; flex-direction: column; align-items: stretch; }
}

/* ==========================================================================
   17. Bandwidth diagram (design 21b, 0.10.41)
   Two rules — a solid compliance floor and a dashed, risk-appetite target —
   with a tinted band between them and six domains plotted as drifting dots.
   The figure is a 2x2 grid: an 88px "lane" (the bracket + band label) beside
   the plot (SVG + the two rule labels), with the axis (domain names) below
   the plot on its own row. Column 1, row 2 is deliberately empty.

   Renders wherever skuridat_bandwidth_diagram() is called — the standalone
   bandwidth-diagram block, or text-visual/figure with their visual set to
   it — never with its own section or two-column pairing; that is always the
   caller's layout.
   ========================================================================== */
.bw {
	display: grid;
	grid-template-columns: 88px minmax(0, 1fr);
}
.bw__lane {
	grid-column: 1;
	grid-row: 1;
	position: relative;
	height: 280px;
}
.bw__plot-wrap {
	grid-column: 2;
	grid-row: 1;
	position: relative;
	height: 280px;
}
.bw__plot { overflow: visible; }

/* The bracket: a vertical stem plus two 9px ticks, flush to the lane's
   right edge, meeting the target/floor rules at plot x=0 as one line. */
.bw__bracket {
	position: absolute;
	right: 0;
	background: color-mix(in srgb, var(--body) 50%, transparent);
}
.bw__bracket--stem { top: 78.4px; height: 117.6px; width: 1.3px; }
.bw__bracket--top { top: 78.4px; width: 9px; height: 1.3px; }
.bw__bracket--bottom { top: 196px; width: 9px; height: 1.3px; }

.bw__band-label {
	position: absolute;
	right: 16px;
	top: 78.4px;
	height: 117.6px;
	display: flex;
	align-items: center;
	justify-content: flex-end;
	text-align: right;
	margin: 0;
	font: 500 10px/1.2 var(--font-mono);
	letter-spacing: .1em;
	text-transform: uppercase;
	color: var(--body);
}

.bw__rule-label {
	position: absolute;
	display: flex;
	flex-direction: column;
	gap: 2px;
}
.bw__rule-label--target { left: 12px; bottom: 209.6px; align-items: flex-start; text-align: left; }
.bw__rule-label--floor { right: 12px; bottom: 92px; align-items: flex-end; text-align: right; }
.bw__rule-title {
	margin: 0;
	font: 500 10.5px/14px var(--font-mono);
	letter-spacing: .1em;
	text-transform: uppercase;
	color: var(--heading);
}
.bw__rule-sub {
	margin: 0;
	font: 400 12.5px/16px var(--font-body);
	color: var(--muted);
}

.bw__axis {
	grid-column: 2;
	grid-row: 2;
	padding-top: 14px;
	border-top: 1.2px solid rgba(168, 203, 207, .45);
	display: grid;
	grid-template-columns: repeat(6, minmax(0, 1fr));
}
.bw__axis-name {
	font: 500 10px/1.3 var(--font-mono);
	letter-spacing: .06em;
	text-transform: uppercase;
	text-align: center;
	color: var(--muted);
}
.bw__axis-name--accent { color: var(--accent); }

/* Narrow fallback for the rule sub-lines (section 21, below). Printed
   always, shown by exactly one of the two rules below. */
.bw__caption { display: none; }
.bw__caption p { margin: 0; font: 400 12px/1.5 var(--font-mono); color: var(--muted); }
.bw__caption p + p { margin-top: 4px; }

/* Motion: dots drift around their rest value; each hairline stretches with
   its dot, on the same easing/duration/delay so the end never detaches.
   Paused until the figure scrolls into view — theme.js reuses the same
   IntersectionObserver the operating-model block uses, toggling
   .is-playing on this root, per the brief. */
.bw-dot,
.bw-dev {
	animation: var(--kf) var(--dur) ease-in-out var(--delay) infinite;
	animation-play-state: paused;
}
.bw.is-playing .bw-dot,
.bw.is-playing .bw-dev {
	animation-play-state: running;
}
.bw-dev { transform-box: view-box; }

@keyframes bw-dot-1 { 0%, 100% { transform: translateY(0); } 25% { transform: translateY(-16.8px); } 75% { transform: translateY(16.8px); } }
@keyframes bw-dot-2 { 0%, 100% { transform: translateY(0); } 25% { transform: translateY(-14px); } 75% { transform: translateY(14px); } }
@keyframes bw-dot-3 { 0%, 100% { transform: translateY(0); } 25% { transform: translateY(-19.6px); } 75% { transform: translateY(19.6px); } }
@keyframes bw-dot-4 { 0%, 100% { transform: translateY(0); } 25% { transform: translateY(-11.2px); } 75% { transform: translateY(11.2px); } }
@keyframes bw-dot-5 { 0%, 100% { transform: translateY(0); } 25% { transform: translateY(-14px); } 75% { transform: translateY(14px); } }
@keyframes bw-dot-6 { 0%, 100% { transform: translateY(0); } 25% { transform: translateY(-11.2px); } 75% { transform: translateY(11.2px); } }

@keyframes bw-dev-2 { 0%, 100% { transform: scaleY(1); } 25% { transform: scaleY(.5833); } 75% { transform: scaleY(1.4167); } }
@keyframes bw-dev-5 { 0%, 100% { transform: scaleY(1); } 25% { transform: scaleY(1.4167); } 75% { transform: scaleY(.5833); } }
@keyframes bw-dev-6 { 0%, 100% { transform: scaleY(1); } 25% { transform: scaleY(.4286); } 75% { transform: scaleY(1.5714); } }

@media (prefers-reduced-motion: reduce) {
	.bw-dot, .bw-dev { animation: none; }
}

/* Below 720: the lane (bracket + band label) is dropped and the plot takes
   the full content width — the design was never made narrower than this,
   so these are working rules, not a spec. */
@media (max-width: 719px) {
	.bw { grid-template-columns: minmax(0, 1fr); }
	.bw__lane { display: none; }
	.bw__plot-wrap { grid-column: 1; }
	.bw__axis { grid-column: 1; }
	.bw__axis-name { letter-spacing: .02em; white-space: normal; }
}

/* Below roughly 500px viewport width the floor label starts to crowd
   Endpoint's dot (the brief's own threshold is a ~464px *plot* width; this
   theme has no container queries set up elsewhere, so it is approximated
   against the viewport, which is a close match once the lane above is
   already hidden). Titles stay in the plot; only the sub-lines move down
   to a caption under the axis. Below roughly 340px the floor title will
   still touch Endpoint — the brief says to flag that rather than solve it
   silently, so it is left as-is. */
@media (max-width: 500px) {
	.bw__rule-sub { display: none; }
	.bw__caption { display: block; margin-top: 12px; }
}

/* ==========================================================================
   18. Phase row
   Five cells on desktop, a horizontal scroller of pills on mobile.
   ========================================================================== */

.phase-band { position: relative; }

/* The tint on its own is translucent, which is right for a band sitting in
   the flow: the page colour behind it is the same colour anyway. */
.phase-band--tint { background: var(--fill-band); padding-block: 32px; }

/* Sticky is different. A translucent background lets the sections scroll
   *through* the bar instead of behind it, which reads as broken. So the
   sticky variant paints the same designed tint over an opaque page colour:
   background-color is the slab, background-image is the tint as a flat
   gradient. Same value, same token, no transparency. */
.phase-band.phase-band--sticky {
	position: sticky;
	/* Below the bar, not behind it. */
	top: calc(var(--bar-top) + var(--nav-h));
	z-index: 50;
	border-bottom: var(--line-section);
	background-color: var(--page);
	background-image: linear-gradient(var(--fill-band), var(--fill-band));
}

/* Past the last phase the bar has done its job and rides up out of the way,
   hooked to phase 05's own bottom divider at scroll speed — theme.js sets
   `transform: translateY()` inline, continuously, to exactly the pixel the
   divider has crossed the band's own edge, not a fixed distance on a
   timer. No transition here: an eased transform fighting a value that is
   already tied 1:1 to the scroll position is what made the old, class-
   toggled version look like it snapped. `.is-released` only marks the
   fully-tucked-away state, once the inline transform already equals the
   band's own height — pointer-events off so a fully hidden band never
   catches a click or a tab stop. */
.phase-band.phase-band--sticky.is-released {
	pointer-events: none;
}


.phase-row { display: grid; grid-template-columns: repeat(5, 1fr); gap: 16px; }

.phase-cell {
	border: var(--line-emph);
	border-radius: var(--radius);
	padding: 20px 12px;
	text-align: center;
	font: 600 15px/1.2 var(--font-display);
	color: var(--body);
	transition: color var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
a.phase-cell:hover { color: var(--heading); }
.phase-cell.is-current { color: var(--heading); border-color: var(--accent); }

/* The Platform page's sticky nav band: one continuous frame instead of five
   separate boxes, a mono "01" index over each label (the same lockup the
   CTEM phase artifacts use for their own in-scene header), and a thin
   accent bar over whichever cell is current — closer to the instrument-
   panel feel of the artifacts beside it than the plain bordered grid the
   teaser row (page-home) still uses, which is untouched below. */
.phase-row--numbered {
	display: grid;
	grid-template-columns: repeat(5, 1fr);
	gap: 0;
	border: var(--line-emph);
	border-radius: var(--radius);
}
.phase-row--numbered .phase-cell {
	border: none;
	border-radius: 0;
	border-left: var(--line-section);
	text-align: left;
	padding: 16px 18px;
	position: relative;
}
.phase-row--numbered .phase-cell:first-child { border-left: none; }
.phase-row--numbered .phase-cell::before {
	content: '';
	position: absolute;
	top: -1.2px;
	left: 0;
	right: 0;
	height: 2px;
	background: var(--accent);
	transform: scaleX(0);
	transition: transform var(--dur) var(--ease);
}
.phase-row--numbered .phase-cell.is-current::before { transform: scaleX(1); }

.phase-cell__num {
	display: block;
	font: 500 10px/1.2 var(--font-mono);
	letter-spacing: .1em;
	text-transform: uppercase;
	color: var(--muted);
	margin-bottom: 6px;
	transition: color var(--dur) var(--ease);
}
.phase-cell__name { display: block; }
.phase-row--numbered .phase-cell.is-current .phase-cell__num { color: var(--accent); }

.payoff {
	font: 600 15px/1.4 var(--font-display);
	color: var(--accent);
	margin-top: 14px;
}

@media (max-width: 860px) {
	.phase-row {
		display: flex;
		gap: 10px;
		overflow-x: auto;
		scrollbar-width: none;
		-webkit-overflow-scrolling: touch;
	}
	.phase-row::-webkit-scrollbar { display: none; }
	.phase-cell {
		flex: 0 0 auto;
		height: 38px;
		display: flex;
		align-items: center;
		padding: 0 16px;
		border-radius: var(--radius);
		font-size: 14px;
	}
	.phase-band--tint { padding-block: 20px; }

	/* The numbered band keeps its cells but drops back to one line: num and
	   name sit side by side (there's no room to stack them at this height),
	   and the current-cell bar moves to a left rule since a rectangle cell
	   has no single top edge to sit on once they're side by side.
	   border-radius stays var(--radius) here too (client call, 28-09-2026:
	   the fully-rounded 19px — half the cell height — read as a pill, which
	   is a different control on this site; every button and cell elsewhere
	   uses the same small corner). */
	.phase-row--numbered {
		border: none;
		border-radius: 0;
		gap: 10px;
	}
	.phase-row--numbered .phase-cell {
		border: var(--line-emph);
		border-radius: var(--radius);
		flex-direction: row;
		align-items: center;
		gap: 8px;
	}
	.phase-row--numbered .phase-cell::before {
		top: 0;
		bottom: 0;
		right: auto;
		left: -1.2px;
		width: 2px;
		height: auto;
		transform: scaleY(0);
	}
	.phase-row--numbered .phase-cell.is-current::before { transform: scaleY(1); }
	.phase-row--numbered .phase-cell.is-current { border-color: var(--accent); }
	.phase-cell__num { margin-bottom: 0; }
}

/* ==========================================================================
   19. Pyramid and diamond
   Bar widths come from the block, not from here. They are the argument.
   ========================================================================== */

.shape { display: flex; flex-direction: column; gap: 8px; margin-top: 16px; align-items: center; }
.shape i { display: block; height: 24px; background: var(--fill-diagram); border: var(--line-emph); border-radius: 1px; }
.shape--left { align-items: flex-start; }

/* ==========================================================================
   20. Cards, pills, logos
   ========================================================================== */

.box--accent { border-color: var(--accent); }
.box--span { grid-column: 1 / -1; }
.box--span p { max-width: 50em; }

a.box { display: block; transition: border-color var(--dur) var(--ease); }
a.box:hover { border-color: rgba(168, 203, 207, .75); }

/* Services' three track cards (`data-card-jump`, page-services.php content):
   fixed states per client request (28-09-2026), replacing the old "the one
   you picked goes solid" behaviour theme.js used to drive on click/arrival.
   Advisory always carries the accent edge; the other two always a plain
   hairline rather than the dashed `.box--soft` "unfilled" treatment, which
   read wrong for cards with real content. None of the three change on
   hover — `a.box:hover`/`.card-jump:hover` are overridden right back to
   the same resting border on all three, so they stay clickable to their
   anchors with no visual state change. `[data-card-jump]` is not used
   anywhere else on the site, so this only reaches these cards. */
a.card-jump[data-card-jump],
a.card-jump[data-card-jump]:hover { transition: none; }
a.card-jump[data-card-jump="advisory"],
a.card-jump[data-card-jump="advisory"]:hover {
	border-color: var(--accent);
}
a.card-jump[data-card-jump="managed-exposure"],
a.card-jump[data-card-jump="managed-exposure"]:hover,
a.card-jump[data-card-jump="offensive-security"],
a.card-jump[data-card-jump="offensive-security"]:hover {
	background: none;
	border: 1.3px solid rgba(168, 203, 207, .35);
}

.cred-row { margin-top: 24px; }
.cred-row--grey .row { filter: grayscale(1); opacity: .7; }
.cred-pill-image { height: 40px; width: auto; }

.logo-strip { display: flex; gap: 28px; margin-top: 20px; filter: grayscale(1); opacity: .62; transition: opacity var(--dur) var(--ease); }
.logo-strip:hover { filter: none; opacity: 1; }
.logo-strip__item { flex: 1; display: flex; align-items: center; justify-content: center; height: 56px; border: var(--line-box); border-radius: var(--radius); background: var(--fill-card); }
.logo-strip__item--text { font: 600 14px/1 var(--font-display); letter-spacing: .08em; color: var(--body); }

/* Name plus discipline is one heading ("Mateus Riad · Offensive security"),
   so a short discipline keeps it on one line and a long one wraps it to
   two — up to two, never more, per the handoff. Client call, 28-09-2026:
   with one founder's heading at one line and the other's at two, the "one
   sentence" paragraph below started at two different heights. 2.4em
   (h3's own 1.2 line-height × 2) reserves room for the two-line case on
   every founder, so the paragraph always starts from the same line
   regardless of how long that founder's own name/discipline happens to be. */
.founder h3 { margin-bottom: 8px; min-height: 2.4em; }

.rule-lines { display: grid; gap: 10px; margin: 16px 0; }
.rule-lines i { display: block; height: 1px; background: rgba(168, 203, 207, .24); }

@media (max-width: 860px) {
	.is-mobile-hidden { display: none; }
	.logo-strip { flex-wrap: wrap; }
	.logo-strip__item { flex: 1 1 40%; }
}

/* ==========================================================================
   21. Articles
   ========================================================================== */

.filters { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 24px; }

.filter-pill {
	height: 40px;
	display: inline-flex;
	align-items: center;
	padding: 0 18px;
	border: var(--line-field);
	border-radius: var(--radius);
	font-size: 13px;
	color: var(--body);
	transition: color var(--dur) var(--ease), border-color var(--dur) var(--ease);
	cursor: pointer;
}
.filter-pill:hover { color: var(--heading); }
.filter-pill[aria-pressed="true"] { color: var(--heading); border-color: var(--accent); }

.article-grid { align-items: start; }
.article-card[hidden] { display: none; }
.article-card__title { margin: 16px 0 8px; font-size: 19px; }
.article-card__dek { margin: 0; font-size: 15px; }
.article-card__meta { margin: 8px 0 0; font-size: 13px; color: var(--muted); }

.byline { display: flex; align-items: center; gap: 12px; margin-top: 24px; }
.byline__avatar { width: 40px; height: 40px; padding: 0; border-radius: 50%; flex: 0 0 auto; }
.byline__avatar--lg { width: 48px; height: 48px; }

.breadcrumb { margin-bottom: 24px; }
.breadcrumb a:hover { color: var(--heading); }

.article-header h1 { margin-bottom: 16px; }
.article-header .lead { margin-bottom: 8px; }

/* The measure is exactly 720px, with the contents rail beside it. */
.article-layout {
	display: grid;
	grid-template-columns: 240px 720px 1fr;
	gap: 64px;
	align-items: start;
}

.article-body { max-width: 720px; }
/* The body headings render as h2.h3 so the outline is right; the selector has
   to match both, because a saved article may still hold a real h3. */
.article-body h3,
.article-body .h3 { font-size: clamp(22px, 18.29px + 0.952vw, 28px); margin: 40px 0 12px; max-width: none; }
.article-body p { max-width: none; }
.article-body p + p { margin-top: 16px; }
.article-body ul { margin: 16px 0; padding-left: 20px; list-style: disc; }
.article-body li { margin-bottom: 8px; }

.contents { position: sticky; top: calc(var(--bar-top) + var(--nav-h) + 32px); }
.contents__mobile { display: none; }
.contents__desktop .contents__list {
	display: grid;
	gap: 10px;
	border-left: var(--line-table);
	padding-left: 20px;
	margin-top: 12px;
}
.contents__list a { font-size: 14px; color: var(--body); }
.contents__list a:hover { color: var(--heading); }
.contents__list a.is-current {
	color: var(--heading);
	box-shadow: -21px 0 0 -20px var(--accent);
}
.contents__share { margin-top: 32px; padding-top: 20px; border-top: var(--line-table); }

.article-figure { margin: 28px 0; }
.article-figure figcaption { margin-top: 12px; }
/* A real image sits where the slot sat, and takes the slot's edge so the two
   states of a figure are the same shape. Solid, not dashed: the slot is a
   boundary waiting to be filled, the image is a thing. */
.article-figure__img {
	display: block;
	width: 100%;
	height: auto;
	border: var(--line-box);
	border-radius: var(--radius);
	background: var(--fill-card);
}

.in-short { margin: 28px 0; }
.in-short h4 { margin-bottom: 8px; }

.case-card h3 { margin-bottom: 8px; }
.case-card[hidden] { display: none; }

@media (max-width: 1100px) {
	.article-layout { grid-template-columns: 1fr; gap: 32px; }
	.article-body { max-width: var(--measure); }
	.contents { position: static; }
	.contents__desktop { display: none; }
	.contents__mobile { display: block; background: var(--fill-band); padding: 16px var(--gutter); margin-inline: calc(var(--gutter) * -1); }
	.contents__summary { cursor: pointer; font-size: 15px; color: var(--heading); list-style: none; }
	.contents__summary::-webkit-details-marker { display: none; }
	.contents__summary::after { content: " ⌄"; color: var(--muted); }
	.contents__mobile .contents__list { display: grid; gap: 12px; margin-top: 16px; }
}

@media (max-width: 860px) {
	/* Article figures go beyond the gutter to the slab edge, with the caption
	   inset back to it. The one place a figure crosses the gutter. */
	.article-figure { margin: 28px calc(var(--gutter) * -1); }
	.article-figure .slot,
	.article-figure img { border-left: 0; border-right: 0; border-radius: 0; }
	.article-figure figcaption { padding-inline: var(--gutter); }
	.article-card--related { display: grid; grid-template-columns: 96px 1fr; gap: 16px; align-items: center; }
	.article-card--related .slot { height: 72px; }
	.article-card--related .article-card__title { margin: 0 0 4px; font-size: 17px; }
}

/* ==========================================================================
   22. Document download
   A policy and its downloadable copy, side by side. Solid card, because it
   is a thing rather than a boundary.
   ========================================================================== */

.doc-download {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--gap-2col);
	flex-wrap: wrap;
	background: var(--fill-card);
	border: var(--line-box);
	border-radius: var(--radius);
	padding: var(--card-pad);
	margin: 40px 0;
	max-width: none;
}

.doc-download__text { flex: 1 1 20em; }
.doc-download__heading { margin-bottom: 6px; }
.doc-download__body { margin: 0; font-size: 15px; max-width: 34em; }
.doc-download__meta { margin-top: 12px; letter-spacing: .1em; }

.doc-download__btn { flex: 0 0 auto; }
.doc-download__icon { font-size: 1.1em; line-height: 1; }

@media (max-width: 860px) {
	.doc-download { display: block; }
	.doc-download__btn { margin-top: 20px; width: 100%; }
}

/* Inside prose the card should not inherit the reading measure. */
.prose .doc-download { max-width: none; }

/* ==========================================================================
   23. Hero media
   Full bleed to the slab edge, parallax on desktop, brand wash on top.

   Two deliberate departures from the handoff, both requested:
   the 8px radius (the spec says 2px everywhere), and the fixed-attachment
   parallax (the spec says no decorative motion). Recorded in the handoff
   README under "Amendments, as built".
   ========================================================================== */

.hero-media {
	position: relative;

	/* Aligned to the text column, not bled to the slab edge. The figure sits
	   inside .wrap and takes no negative margin, so its left and right edges
	   line up with the headline above it at every width. */
	border-radius: var(--hero-radius, 8px);
	overflow: hidden;

	/* Small source by default. The desktop file is only fetched above the
	   breakpoint, so a phone never downloads the 1440px image. */
	background-image: var(--hero-img-sm);
	background-size: cover;
	background-position: center;
	background-repeat: no-repeat;
	background-color: var(--hero-ground);
}

/* The wash. Page colour on the left, accent on the right.

   The two colours are fixed: page and accent, the only two the brand has.
   The two opacities are a named choice on the block, not a free control,
   so an editor can dial the wash back on a photograph that needs to be
   read without being able to introduce a third colour. */
.hero-media::after {
	content: "";
	position: absolute;
	inset: 0;
	background: linear-gradient(
		90deg,
		rgba(23, 46, 52, var(--hero-wash-start, .30)) 0%,
		rgba(252, 86, 29, var(--hero-wash-end, .70)) 100%
	);
	pointer-events: none;
}

/* The four named strengths. Specificity is doubled on purpose: source order
   must not be able to break these quietly the way it did the phase band. */
.hero-media.hero-media--wash-full   { --hero-wash-start: .30; --hero-wash-end: .70; }
.hero-media.hero-media--wash-medium { --hero-wash-start: .26; --hero-wash-end: .52; }
.hero-media.hero-media--wash-light  { --hero-wash-start: .22; --hero-wash-end: .34; }
.hero-media.hero-media--wash-none::after { display: none; }

.hero-media--feature { height: 480px; }
.hero-media--inline  { height: 320px; }

@media (max-width: 860px) {
	.hero-media--feature { height: 240px; }
	.hero-media--inline  { height: 200px; }
}

/* Parallax, desktop only.

   background-attachment: fixed is broken on iOS Safari and janky on most
   touch devices, which is why it is scoped to the desktop breakpoint rather
   than switched off for phones as an afterthought.

   Maintenance trap: a transform, filter or will-change on ANY ancestor makes
   the background fix to that ancestor instead of the viewport, and the
   effect silently dies. If parallax ever stops working, look up the tree
   before looking here. */
@media (min-width: 861px) {
	.hero-media {
		background-image: var(--hero-img);
		background-attachment: fixed;
	}
}

/* Parallax off, per block. Doubled specificity so it wins over the desktop
   rule above wherever this ends up in the file.

   A fixed background is sized against the viewport rather than the element,
   so switching this off also changes the crop: a still image fills its own
   box, a parallax one shows a window onto a viewport-sized image. That is
   inherent to background-attachment, not a bug in the toggle. */
.hero-media.hero-media--still { background-attachment: scroll; }

/* Parallax is a known vestibular trigger. Anyone who has asked their system
   to stop moving things gets a still image. This has to stay last: it
   overrides the block's own choice, and that is the correct precedence. */
@media (prefers-reduced-motion: reduce) {
	.hero-media,
	.hero-media.hero-media--parallax { background-attachment: scroll; }
}

/* ==========================================================================
   24. Phase 3 — form states, and the modal without JavaScript
   Nothing here introduces a colour or a border. It resolves two display
   conflicts and gives the no-JS path a way to open the dialog.
   ========================================================================== */

/* Every request CTA is a plain #request link until theme.js intercepts it, so
   :target is what opens the modal with JavaScript off. theme.js removes
   [hidden] instead and never sets the fragment, so the two paths never fight
   over the same element. Must stay after .modal[hidden]. */
.modal:target { display: flex; }

/* .form-panel sets display:grid, which outranks the UA stylesheet's rule for
   [hidden]. Without this the form stays visible behind its own success panel
   after a submit. */
.form-panel[hidden] { display: none; }

/* The gated paper's panel sits in a section, not in the modal grid, so its
   success state needs the same full-width treatment the modal's gets. */
.gated .form-success { grid-column: auto; }

/* The dialog takes focus on open so a screen reader announces its title. It
   is tabindex="-1" and never keyboard-reachable, so the focus ring would only
   ever be a 2px accent outline around the whole dialog after a keyboard
   interaction. Every control inside it keeps its own ring. */
.modal__dialog:focus,
.modal__dialog:focus-visible { outline: none; }

/* theme.json's link colour lives in the global-styles output, which
   inc/assets.php dequeues, so `a { color: inherit }` wins unless a component
   says otherwise. Every other link on the site already does; this is the
   modal's privacy link. */
.modal__scope .caption a { color: var(--accent); }
.modal__scope .caption a:hover { filter: brightness(1.15); }

/* Mobile head, per 8g: a plain title row rather than the desktop's accent
   eyebrow, which at 390 reads as a section label instead of a dialog title.
   The subline stays: the wireframe drops it for space, but the sheet has
   room and it answers the first question a visitor has. */
@media (max-width: 860px) {
	.modal__head .eyebrow {
		font-size: 15px;
		letter-spacing: -.01em;
		text-transform: none;
		color: var(--heading);
		margin-bottom: 4px;
	}
	.modal__subline { font-size: 14px; color: var(--muted); }
}

/* ==========================================================================
   25. Phase 4 — block layout helpers
   Two utility classes the section blocks set. No colour, no border: those
   come from the tokens above and from nowhere else.
   ========================================================================== */

/* The two-column sections in the design align on the vertical centre. It is a
   property of the layout, not something an editor picks, so the blocks set it
   and there is no control for it. */
.grid2.is-centred,
.grid3.is-centred { align-items: center; }

/* Cards past the mobile cap. The landing grid shows six on desktop and four
   at 390 — the design drops the last two rather than shrinking all six. */
@media (max-width: 860px) {
	.card--capped { display: none; }
}

/* The landing teaser puts the five cells between its heading and the
   two-column explanation under it. The rhythm is the design's, not the
   editor's, so it lives here. */
.phase-row-wrap { margin: 40px 0 32px; }

/* A pill inside a card shares the row evenly. Three EU pills across one card
   is the only place this is used, and it should not need an inline width. */
.slot--pill.is-flexible { flex: 1; }
.media--pill.is-flexible { flex: 1; }

/* A pill holding a real photo instead of its text label: the same 56px
   height as the placeholder slot beside it (skuridat_image_or_slot() can
   render either at the same position in the row), cropped to fill it
   rather than the image's own aspect ratio — a pill row that jumped
   height per photo would look broken next to the ones still showing
   their fixed-height placeholder. */
.media--pill { height: 56px; border-radius: var(--radius); }
.media--pill .media__img { height: 100%; object-fit: cover; }

/* Pills under a card's paragraph. 18px, which is the gap the design has —
   .cred-row's 24px is for a pill row that follows a heading, not a sentence. */
.card-pills { margin-top: 18px; }

/* ==========================================================================
   25b. Meta pills (0.11.42)
   The services page's six meta pills (Duration/Led by/Retainer/Team/
   Pricing/Certified) and the "European by architecture" card's three EU
   pills, once each has a caption (skuridat_detail_pills()/skuridat_meta_pill()
   in inc/template-tags.php) — a small data card instead of the plain
   dashed placeholder, 3:1 like every card in the reference. `.row`'s own
   flex layout is overridden with a grid here: these are cards with real
   height, not small flex-sized text pills.
   ========================================================================== */
.detail-pills--meta,
.card-pills--meta { display: grid; gap: 20px; }
.detail-pills--meta { grid-template-columns: 1fr 1fr; }
.card-pills--meta { grid-template-columns: repeat(3, 1fr); }

/* card-pills--meta sits inside a card that is itself half of a two-column
   grid (card-grid's own .grid2), so three of these across get a lot less
   room than the 860px page-wide breakpoint (section 2) accounts for —
   measured live at a 1024px viewport (a real laptop width, not an edge
   case): each pill came out under 120px, not enough for a ring plus even
   a heavily-abbreviated value. Below 1300px the row stacks to one column,
   each pill then getting the card's full, comfortable width; the plain
   860px breakpoint still applies above this one, for when the page itself
   goes single-column and the card-grid .grid2 stops halving anything. */
@media (max-width: 1300px) {
	.card-pills--meta { grid-template-columns: 1fr; }
}

/* Client call, 29-09-2026: border and fill now match .detail-panel — the
   frame around "Typical scope"/"You get"/"Business outcome" on this same
   page — exactly, `--line-slab` and `--fill-card`, rather than the
   heavier `--line-box` cards elsewhere use. Only the top edge stays the
   2px accent stripe; every other edge reads as "the block beside it".
   Fixed height, not aspect-ratio: 3/1 — client call, so every card in a
   services-page group matches "Led by"'s height exactly, which content-
   driven sizing (a value that wraps to two lines, say) can't guarantee
   on its own. 132px fits the tallest real content (a two-line value
   above a badge-row visual) with some air to spare. */
.metapill {
	height: 132px;
	border: var(--line-slab);
	border-top: 2px solid var(--accent);
	border-radius: var(--radius);
	background: var(--fill-card);
	padding: 18px 22px;
	display: flex;
	flex-direction: column;
	gap: 14px;
	min-width: 0;
}
.metapill__stack { min-width: 0; margin-bottom: auto; }
.metapill__label {
	font: 600 11px/1 var(--font-display);
	letter-spacing: .12em;
	text-transform: uppercase;
	color: var(--muted);
}
/* Fixed, not clamp(): a viewport-relative size read differently at every
   width and still needed a fixed floor/ceiling either side, which made it
   no more predictable than picking the one size that works — measured
   live against the services page's own two-column pill width so
   "AI-augmented core team" and "retained · monthly" (the two longest
   values) keep to one line there, same as the shorter ones. 19px is the
   largest of those that still fits "AI-augmented core team" (the widest
   value) with room to spare — 20px was a few pixels over. */
.metapill__value {
	font: 600 19px/1.1 var(--font-mono);
	letter-spacing: -.01em;
	color: var(--heading);
}
/* 32px, not 20px, and the same figure for every kind including
   --stack (duration) below: the value's own centering
   (:not(.metapill--eu) .metapill__value, further down) measures "the
   middle" from how much room .metapill__visual claims, so if that
   claim varies by kind — a badge chip actually runs taller than the
   thin pricing line, whatever height each got assigned — the value
   lands at a different height in every card. One fixed claim for all
   six keeps that math identical regardless of which decoration a
   given position renders. 32px comfortably fits the tallest of them,
   the badge chips, without clipping. */
.metapill__visual { display: flex; align-items: center; height: 32px; }
.metapill__visual--stack { flex-direction: column; align-items: stretch; justify-content: center; }

/* Client call, 29-09-2026: the value should sit centred in the space
   between the label and the visual, not pinned just under the label.
   Only for the six services pills — the EU pills keep the value flush
   under the label beside the ring, .metapill--eu overrides this back.
   The stack grows to fill whatever room `.metapill__visual` doesn't need
   (flex: 1, replacing the old margin-bottom: auto, which only pushed
   from below and left the value sitting flush under the label); inside
   that grown stack, margin-top/bottom: auto on the value itself centres
   it in the space the label doesn't use — the same auto-margin
   centering trick as a lone flex child, just nested one level in. */
.metapill:not(.metapill--eu) .metapill__stack {
	display: flex;
	flex-direction: column;
	flex: 1;
	min-height: 0;
	margin-bottom: 0;
}
.metapill:not(.metapill--eu) .metapill__value { margin-top: auto; margin-bottom: auto; }

/* Duration: an 8-segment bar, solid for the first half and hatched for the
   second — a fixed "in progress toward the later week" motif, not a literal
   plot of the value. The end label reads the value's own trailing number
   (skuridat_meta_pill_visual()), so "4–8 weeks" ends the bar at "W8". */
.viz-duration { display: flex; align-items: flex-end; gap: 4px; width: 100%; }
.viz-duration .seg { flex: 1; height: 8px; border-radius: 1px; background: var(--accent); }
.viz-duration .seg.is-future {
	background: repeating-linear-gradient(45deg, var(--accent) 0 3px, transparent 3px 6px);
	opacity: .55;
}
.viz-duration__labels { display: flex; justify-content: space-between; width: 100%; font: 500 10px/1 var(--font-mono); color: var(--muted); margin-top: 4px; }

/* The EU card's ring, holding the value's own leading acronym ("EU" —
   skuridat_meta_pill_visual()'s regex), not a fixed icon, so it stays
   right if the wording ever changes. This is the only kind still using a
   ring — "Led by" moved to the rectangular badge style below (client
   call, 29-09-2026), since its own value ("led by GRC architect") starts
   lowercase and had nothing for the ring's regex to grab.

   36px: went 40 → 46 ("a little bigger") → back down to 36 ("kleiner
   maar leesbaar") once 46px, next to the shorter value text this card's
   own width forces, read as crowding it rather than reading as data. */
.viz-badge-ring {
	width: 36px; height: 36px; border-radius: 50%;
	border: 1.3px solid var(--accent);
	display: flex; align-items: center; justify-content: center;
	flex: none;
	font: 600 9px/1 var(--font-mono); color: var(--accent);
}

/* Retainer: a row of dots, a fixed "recurring" motif. */
.viz-dots { gap: 10px; }
.viz-dots i { width: 6px; height: 6px; border-radius: 50%; background: var(--accent); display: block; }

/* Team: two outline rings (the always-on core) and a solid diamond (senior
   expertise, drawn in) — the same three-tier language operating-model's
   own legend uses, in miniature. */
.viz-team { gap: 10px; }
.viz-team .ring { width: 16px; height: 16px; border-radius: 50%; border: 1.3px solid var(--body); }
.viz-team .diamond { width: 11px; height: 11px; background: var(--accent); transform: rotate(45deg); }

/* Pricing: a range line with a dot at each end.

   No height here (previously 8px, just enough for the dots): `.viz-range`
   and `.metapill__visual` are the same element, and a same-specificity
   height on each — 8px here, the shared 32px on `.metapill__visual` —
   was won by whichever happened to sit later in the file, which is
   exactly the kind of accident that made the six pills' values line up
   at a different height depending on which decoration each row's
   position happened to render (the bug this fixes). The line and its
   dots already centre themselves (top: 50% / align-items: center)
   regardless of the row's own height, so they don't need to set it. */
.viz-range { width: 100%; position: relative; }
.viz-range::before { content: ""; position: absolute; left: 4px; right: 4px; top: 50%; height: 1.3px; background: var(--accent); transform: translateY(-50%); }
.viz-range i { width: 8px; height: 8px; border-radius: 50%; background: var(--accent); position: relative; z-index: 1; }
.viz-range i:last-child { margin-left: auto; }

/* Certified: the value's own all-caps tokens ("OSCP · CISSP led" → OSCP,
   CISSP — skuridat_meta_pill_visual()'s regex), each in its own outline
   badge, rather than a fixed pair that would drift from the text above it. */
.viz-badges { gap: 8px; }
.viz-badges span {
	border: 1.3px solid var(--accent);
	color: var(--accent);
	font: 600 10px/1 var(--font-mono);
	padding: 5px 9px;
	border-radius: var(--radius);
}

/* The EU pills read left to right (caption+value, then the ring) rather
   than stacking value-over-visual like the six above — three across suits
   a row better than three decorative footers would, and it's a shorter,
   single-row card, so it drops the fixed 132px height the six services
   pills share and sizes to its own (smaller) content instead.

   Three of these share a row inside the card-grid card's own column,
   which is itself half of a two-column grid (.grid2) — a narrower,
   viewport-tracking width an ordinary breakpoint doesn't catch: at the
   full 1440px slab there's room to spare, but well before the mobile
   single-column layout (860px, section 2) a full-size value plus ring
   already crowd the ~150px a pill gets there. Fixed, smaller sizes
   throughout (client call, 29-09-2026, replacing the first pass's
   clamp()s) rather than scaling with the viewport — the ellipsis stays
   as a hard backstop, not the first line of defence. */
.metapill--eu {
	height: auto;
	padding: 14px 16px;
	flex-direction: row;
	align-items: center;
	justify-content: space-between;
	gap: 10px;
}
.metapill--eu .metapill__stack { display: flex; flex-direction: column; min-width: 0; margin-bottom: 0; }
/* "Set de kopregel 2px hoger": the label sits 2px closer to the card's
   own top edge than the six services pills' labels do. */
.metapill--eu .metapill__label { margin-top: -2px; }
.metapill--eu .metapill__value {
	font-size: 15px;
	margin-top: 4px;
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
}

/* Matches the 860px breakpoint the surrounding .grid2/.grid3 grids already
   use (section 2), not an independent one: the card-grid card this row
   sits in goes full-width there too, so the two breakpoints landing
   together avoids an in-between range where the outer card is still
   two-up but this row has already run out of room. */
@media (max-width: 860px) {
	.detail-pills--meta,
	.card-pills--meta { grid-template-columns: 1fr; }
	.metapill { height: auto; min-height: 120px; }
}

/* A closing CTA is normally one paragraph. Platform's is two, so they need a
   gap between them; p + p already gives 16px, this only holds the measure. */
.closing-cta__body { max-width: 32em; }

/* ==========================================================================
   26. Phase 5 — inner pages
   ========================================================================== */

.page-hero__media { margin-top: 48px; }

@media (max-width: 860px) {
	.page-hero__media { margin-top: 20px; }
}

/* Detail columns. Boxed ones are .box and need nothing; plain ones are text
   columns, so only their rhythm is set here. */
.detail-cols { margin-top: 32px; }

/* The split layout. Heading, lead and pills down the left, the same columns
   stacked down the right, both sides stretching to the taller of the two so
   the pills sit on the foot of the section rather than under the paragraph
   that happens to be shortest. Wireframe 12a. */
.detail-split { align-items: stretch; margin-top: 24px; }
.detail-split__text { display: flex; flex-direction: column; }
.detail-stack { display: flex; flex-direction: column; gap: 20px; }

/* The meta pills close the left column on the panel's bottom edge, and they
   are taller than a standard pill so the space above them stays in
   proportion. The two go together: at 56px the slots end 72px short of the
   frame and the column reads as unfinished, and no amount of alignment fixes
   that on its own. Wireframe 12d. */
.detail-split__text .detail-pills { margin-top: auto; padding-top: 32px; }
.detail-split--panel .detail-pills .slot { height: 96px; }

/* The fused panel. The three columns stop being three cards and become three
   rows of one document: one frame, dashed rules between the rows.

   The frame is the 1.2px solid hairline rather than the card's 1.3px at .5,
   because a panel that outweighs the cards elsewhere on the page reads as a
   different kind of object. The rules inside it are dashed, because a row
   boundary is a boundary, which is what the line system says dashed means.
   Wireframe 12d. */
.detail-panel {
	border: var(--line-slab);
	border-radius: var(--radius);
	background: var(--fill-card);
}
.detail-panel__row { padding: var(--panel-row-y) var(--panel-row-x); }
.detail-panel__row + .detail-panel__row { border-top: var(--line-section); }

/* The row labels, and the last one is the accent. The three rows are not
   three equal facts: scope and deliverable are what the engagement contains,
   the outcome is what it is for, and it is the row a reader should leave with.
   The wireframe marks it and the site follows.

   The accent is not doing the work alone: the row is last, it is the only one
   whose label is a conclusion, and the label reads "Business outcome". Colour
   is the fourth signal, not the only one. */
.detail-panel__label {
	font: 600 11px/1.2 var(--font-display);
	letter-spacing: .14em;
	text-transform: uppercase;
	color: var(--muted);
	max-width: none;
	margin-bottom: 8px;
}
.detail-panel__row:last-child .detail-panel__label { color: var(--accent); }
.detail-col__media { margin-top: 24px; }
.detail-cols__media { margin-top: 40px; }

/* eu-stack folds a heading, body copy and a ticker into the same section
   as the animation, right after it. Client feedback: 40px (the gap
   `.detail-cols__media` uses before a full-width ticker/diagram that
   follows column content) read as too tight once there was a heading of
   its own sitting right under the diagram — they wanted the same
   breathing room a section normally gives its own heading, which is
   `--sec-pad`, the section's own top padding, not a fixed pixel value
   tuned for a different kind of content stack. */
.eu-stack__ticker { margin-top: var(--sec-pad); }
.detail-pills { margin-top: 24px; }
.detail-col .shape { margin-top: 24px; }

/* Founders. The portrait sits above the name at the inline slot height, and
   the credential pills close the card. */
.founders { margin-top: 40px; }

/* The portrait sits beside the person rather than above them. Two founders
   across, each one a portrait and a paragraph, which is what the section is
   claiming: two disciplines, side by side. Wireframe 7f.

   0.11.40, client layout: the portrait column is a fixed 200px — the
   portrait's own fixed 200x200 size, not a fraction of it — and the text
   takes what is left, so the two founders' text columns are the same width
   whatever their portraits are. `align-items: stretch` (was `start`): safe
   now that the portrait is a fixed pixel box rather than an aspect-ratio one
   that grid stretch could distort, and it's what lets the media column's
   badges reach the row's own full height — see `.founder__media` below. */
.founder {
	display: grid;
	grid-template-columns: 200px minmax(0, 1fr);
	gap: 28px;
	align-items: stretch;
}

/* The portrait's own fixed box (0.11.40) — 200x200, not a fraction of the
   column, so the placeholder slot and a real portrait always occupy exactly
   the same square regardless of what's uploaded. */
.founder__portrait { width: 200px; height: 200px; position: relative; }
.founder__portrait img,
.founder__portrait .media,
.founder__portrait .slot { width: 100%; height: 100%; object-fit: cover; border-radius: var(--radius); }

/* Portrait and badges share one column, stretched to the grid row's full
   height by `.founder`'s own `align-items: stretch` above. The portrait
   keeps its fixed 200x200 regardless; `margin-top: auto` on the badges is
   what actually uses that extra height, pushing them down to sit level
   with the bottom of the text column beside them — usually a real gap
   under the portrait, since 200px is shorter than most bios (0.11.40). */
.founder__media { display: flex; flex-direction: column; }
.founder__creds { margin-top: auto; padding-top: 16px; justify-content: space-between; gap: 0; }

/* At phone width the portrait goes back above the person: the media column
   becomes its own row, stacked above the text rather than beside it, so the
   badges sit at their natural height under the portrait rather than
   stretched to match the text — there's no "row" to stretch to anymore. */
@media (max-width: 860px) {
	.founder { grid-template-columns: 1fr; gap: 20px; }
}

/* The integrations row on Platform. The one size outside the three-size
   scale, in the design, used exactly once. */
.slot--strip { height: 80px; }

/* A card that ends in an action. */
.card-cta { margin-top: 20px; }
.card-cta .row { margin-top: 0; }

/* The sticky band is also its own section wrapper, so it needs the section's
   dashed divider back and its own vertical rhythm rather than the section's.
   Both were on the inner element before it was flattened. */
.section.phase-band--tint { padding-block: 32px; }

@media (max-width: 860px) {
	.section.phase-band--tint { padding-block: 20px; }
}

/* ==========================================================================
   27. Phase 6 — articles and Resources
   Section 21 already carries the article layout, the contents rail and the
   card typography; this is what the blocks need on top of it. Nothing here
   introduces a colour or a border value — both come from the tokens.
   ========================================================================== */

/* .btn sets display:inline-flex, which outranks the UA stylesheet's rule for
   [hidden] — the same conflict section 24 resolves for .form-panel. Load more
   hides itself when there is nothing left to load, and without this it would
   sit there offering a page that does not exist. */
.btn[hidden] { display: none; }

/* A filter is a real link as well as a filter, so it carries aria-current
   rather than aria-pressed. Same treatment either way: a link that behaves
   like a pressed button should not look like a different control. */
.filter-pill.is-current,
.filter-pill[aria-current] { color: var(--heading); border-color: var(--accent); }

/* One link per card, stretched over the whole card. The headline carries it,
   so a screen reader reads a link with a real name rather than "image". */
.article-card { position: relative; }
.article-card__title a { color: inherit; }
.article-card__title a::after { content: ""; position: absolute; inset: 0; }
.article-card__title { transition: color var(--dur) var(--ease); }
.article-card:hover .article-card__title { color: var(--accent); }

.article-card__media img {
	width: 100%;
	height: 180px;
	object-fit: cover;
	border-radius: var(--radius);
	display: block;
}

.article-grid__more { text-align: center; margin-top: 48px; }
.article-grid__empty { grid-column: 1 / -1; margin: 0; }

/* Related cards run at the same shape as grid cards on desktop; section 21
   turns them into 96×72 rows at 390. */
.article-grid--related { margin-top: 32px; }

/* Featured piece. The image is the inline slot's height, so a real image
   fills the same box the placeholder does and swapping one in moves
   nothing. */
.featured-article__media img {
	width: 100%;
	height: 320px;
	object-fit: cover;
	border-radius: var(--radius);
	display: block;
}

/* A headline that is also a link keeps the heading colour until hover — the
   accent is for the CTA, not for every title on the page. */
.h2--page a { color: inherit; }
.h2--page a:hover { color: var(--accent); }

.byline__line { margin: 0; font-size: 14px; max-width: none; }
.byline__avatar img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; display: block; }

.article-header__media { margin-top: 48px; }
.article-header__media img { width: 100%; height: 480px; object-fit: cover; border-radius: var(--radius); display: block; }

/* The author block closes the piece: 88px portrait beside two lines. */
.author-box {
	margin-top: 48px;
	display: flex;
	gap: 24px;
	align-items: center;
}
.author-box__portrait { width: 88px; height: 88px; padding: 0; flex: 0 0 auto; }
.author-box__portrait img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; display: block; }
.author-box h4 { margin-bottom: 6px; }
.author-box .caption { margin-top: 8px; }

/* The gated paper's panel sits in a grid cell, not in the modal, so it needs
   nothing but its own width. */
.gated__form { width: 100%; }

@media (max-width: 860px) {
	.article-card__media img { height: 96px; }
	.featured-article__media img { height: 200px; }
	.article-header__media { margin-top: 20px; }
	.article-header__media img { height: 240px; }

	/* Keep reading becomes 96×72 rows at 390. The thumbnail spans both text
	   rows so the headline and the meta line stack beside it. */
	.article-card--related .article-card__media { grid-row: 1 / span 2; }
	.article-card--related .article-card__media img { height: 72px; }
	.article-card--related .article-card__meta { grid-column: 2; margin-top: 4px; }

	/* The filter row becomes a horizontal scroller at 390, the same treatment
	   the five-phase row gets. Wrapped onto three lines it reads as a list of
	   options rather than one control. */
	.filters {
		flex-wrap: nowrap;
		gap: 10px;
		overflow-x: auto;
		scrollbar-width: none;
		-webkit-overflow-scrolling: touch;
	}
	.filters::-webkit-scrollbar { display: none; }
	.filter-pill { flex: 0 0 auto; height: 38px; padding: 0 16px; }

	.author-box { gap: 14px; align-items: flex-start; }
	.author-box__portrait { width: 64px; height: 64px; }
	.article-grid__more { margin-top: 24px; }
}

/* ==========================================================================
   28. Phase 7 — use cases
   Section 21 already has .case-card. This is the filter groups, the expanded
   case and the proof box. No colour or border value here either; both come
   from the tokens.
   ========================================================================== */

/* A card is a link, so it needs the link colour off and the box treatment on.
   The selected one is solid, the rest soft, which is the line system doing
   its job: the case you are reading is a thing, the others are options. */
.case-card,
.card-jump { color: inherit; display: block; }
.case-card:hover,
.card-jump:hover { border-color: var(--accent); }
.case-card.is-current { border-color: var(--accent); }
/* The Services jump cards mark the picked card by going solid only, with no
   accent edge: the client's call (0.10.23). */

.case-grid { margin-top: 40px; }
.case-grid__empty { grid-column: 1 / -1; margin: 0; }

/* The three group pills, then the open group's terms under them. The terms
   row is indented by nothing: it is a second row of the same control, not a
   submenu. */
.case-filters { margin-top: 24px; }
.case-filters__terms { margin-top: 12px; }
.case-filters__terms[hidden] { display: none; }

.filter-pill[aria-expanded="true"],
.filter-pill.is-open { color: var(--heading); border-color: var(--accent); }

/* A term pill is the same control one level down, so it is the same pill at
   a smaller size rather than a different-looking thing. */
.filter-pill--term { height: 36px; font-size: 13px; }

.filter-clear {
	height: 36px;
	padding: 0 12px;
	border: 0;
	background: none;
	font-size: 13px;
	color: var(--muted);
	cursor: pointer;
}
.filter-clear:hover { color: var(--heading); }
.filter-clear[hidden] { display: none; }

/* The expanded case. Two three-column rows with the diagram between them. */
.case-detail[hidden] { display: none; }
.case-detail__row { margin-top: 32px; }
.case-detail__diagram { margin-top: 32px; }

/* The diagram image (0.11.38), 3000x1000 — 3:1, set on the wrapper rather
   than the image so a case without one yet still keeps the slot's own
   height (.slot--feature, section 6) instead of collapsing. object-fit:
   cover crops a differently-shaped upload to 3:1 rather than squeezing it. */
.case-detail__diagram .media { aspect-ratio: 3 / 1; }
.case-detail__diagram .media__img { width: 100%; height: 100%; object-fit: cover; }

/* The proof box. The two rules are where a quote or a metric goes; drawing
   them is what makes the shape of the missing thing visible. */
.proof-box__lines { display: grid; gap: 8px; margin: 16px 0; }
.proof-box__lines i {
	display: block;
	height: 9px;
	border-radius: 5px;
	background: rgba(168, 203, 207, .22);
}
.proof-box__lines i:last-child { width: 60%; }

@media (max-width: 860px) {
	.case-grid { margin-top: 20px; }
	.case-detail__row { margin-top: 20px; }
	.case-detail__diagram { margin-top: 20px; }

	/* Both filter rows scroll sideways at 390, like the five-phase row. */
	.case-filters .filters {
		flex-wrap: nowrap;
		overflow-x: auto;
		scrollbar-width: none;
	}
	.case-filters .filters::-webkit-scrollbar { display: none; }
	.filter-clear { flex: 0 0 auto; }
}

/* ==========================================================================
   29. Filter empty states
   Every pill looks the same and behaves the same. A topic with nothing filed
   under it answers with a sentence rather than by looking switched off.
   ========================================================================== */

.article-grid__empty[hidden] { display: none; }

/* A filter filters the page. The lead section steps aside while a topic is in
   force, and takes its section rule with it, so the hero meets the grid on one
   line instead of two. Explicit because .section carries its own box. */
.section[hidden] { display: none; }

/* ==========================================================================
   30. Social and share rows
   The footer links to Skuridat's profiles; the article row shares the piece
   being read. Same tile, two jobs, one set of rules.

   A tile is a filled thing, so its edge is solid, and it takes the button's
   hover: the accent fill, not a brighter grey. The glyph is currentColor, so
   the icon follows the text colour and never needs a second rule.
   ========================================================================== */

/* The list reset, and nothing else. An earlier version set `margin: 0` here
   and the footer row borrowed .cred-row for its spacing; .social is later in
   the file at the same specificity, so it silently cancelled the 24px and the
   icons sat almost against the tagline. Each of the three places this row
   appears now states its own offset, one step more specific. */
.social {
	display: flex;
	flex-wrap: wrap;
	gap: 8px;
	margin: 0;
	padding: 0;
	list-style: none;
}

.social__link {
	position: relative;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 40px;
	height: 40px;
	border-radius: var(--radius);
	background: var(--fill-card);
	border: var(--line-table);
	color: var(--heading);
	transition: background-color var(--dur) var(--ease),
		border-color var(--dur) var(--ease),
		color var(--dur) var(--ease);
}

.social__icon { width: 18px; height: 18px; display: block; }

.social__link:hover,
.social__link:focus-visible {
	background: var(--accent-fill);
	border-color: var(--accent-fill);
	color: #fff;
}

/* Copy link: the confirmation, which theme.js reveals for a beat. A tooltip
   above the tile rather than a label beside it, because the row is four
   tiles wide on a phone and has nowhere to put a word. */
.social__toast {
	position: absolute;
	bottom: calc(100% + 8px);
	left: 50%;
	transform: translateX(-50%);
	padding: 5px 9px;
	border-radius: var(--radius);
	background: var(--heading);
	color: var(--ground);
	font: 600 11px/1.2 var(--font-mono);
	letter-spacing: .08em;
	text-transform: uppercase;
	white-space: nowrap;
	opacity: 0;
	pointer-events: none;
	transition: opacity var(--dur) var(--ease);
}
.social__link.is-copied .social__toast { opacity: 1; }

/* Under the footer tagline: the designed 24px, restated here so it cannot be
   cancelled by the reset above. */
.site-footer .social { margin-top: 24px; }

/* In the article byline the row sits at the end of the line. */
.byline__share.social { margin-left: auto; }

/* In the contents rail it sits under the Share caption. */
.contents__share .social { margin-top: 10px; }

/* Until a booking URL is set, the line reads as text rather than a dead link. */
.talk-first__pending { color: var(--muted); }

/* ==========================================================================
   30. The Company pages
   Careers, Contact, Responsible disclosure, Privacy, Terms. They were a
   single 578px column against the left edge of a 1440px slab, which is the
   one shape on this site that looks like a different site. They now borrow
   the article's layout, which was designed for exactly this: a header
   section, then the sticky rail beside the text.

   Almost nothing here is new. .article-layout, .contents and the 1100px
   collapse all come from section 22, and that is the point — these pages
   should line up column for column with an article, not approximately.
   ========================================================================== */

/* Deliberately the same two rules the article header uses. The lead keeps the
   30em it has everywhere else rather than stretching to the body column
   below it, because the article does not do that either. */
.prose-header h1 { margin-bottom: 16px; }

/* The title band on the Company pages is half a section tall: half the
   padding above the eyebrow and half below the title (0.10.24). These are
   short pages that start with a document, and a full section's air above
   and below one word pushed the text below the fold on a laptop. */
.section.prose-header { padding-block: calc(var(--sec-pad) / 2); }
.prose-header .lead { margin-bottom: 0; }

/* Inside the layout the text takes the article's measure, not the narrower
   reading measure .prose sets for a column standing on its own. */
.article-layout .prose--wide { max-width: 720px; }
.article-layout .prose--wide > *:first-child { margin-top: 0; }

/* The rail and the body text should start on the same grid row, and the
   rule above tries to guarantee it by zeroing whichever element comes
   first. It missed one thing: `wp:post-content` (used by both prose-page
   and article-body) wraps the real content in its own
   `.entry-content` div, a block-formatting-context box that does not let a
   child's margin collapse out through it. The rule above zeroed that
   wrapper's own margin (already zero), while the actual first heading one
   level inside it kept its full margin-top — 56px on a Company page's first
   h2, 40px on an article's — so the body text always started below the
   rail, not level with it. This reaches one element deeper, into whichever
   container (.prose--wide on the five Company pages, .article-body on an
   article) actually holds the wrapper. Confirmed against the live DOM on
   /privacy/, /terms/ and an article, at 1440px, 0.10.32. */
.article-layout .entry-content > *:first-child { margin-top: 0; }

/* Paragraphs cap at the reading measure everywhere on the site, which inside
   a 720px column leaves them 160px short of the headings, the lists and the
   table beside them. One column, one edge. Same rule .article-body carries. */
.prose--wide p { max-width: none; }

/* A page with one heading has no rail, so the text takes the rail's column
   too rather than leaving a 240px hole where a rail would have been. */
.article-layout--norail { grid-template-columns: 720px 1fr; }

/* A grid item will not shrink below its content's minimum unless told to, and
   the processor table on the privacy page is wider than a 390px screen. Left
   alone it pushes the column, the column pushes the gutter, and the whole page
   scrolls sideways by the two pixels the table is over. */
.article-layout > * { min-width: 0; }
.prose .wp-block-table { overflow-x: auto; }

@media (max-width: 1100px) {
	.article-layout--norail { grid-template-columns: 1fr; }
	.article-layout .prose--wide { max-width: var(--measure); }
}

/* ==========================================================================
   31. In-page jumps, and the admin bar
   Services' three summary cards jump to the track sections below them. The
   cards borrow the use-case card's states (section 28); this is where they
   land, and how.
   ========================================================================== */

/* Smooth only on a page that has jump cards, so the skip link and the
   article contents rail keep the instant jump they were tested with.
   Reduced motion already forces scroll-behavior back to auto (section 12). */
html:has(.card-jump) { scroll-behavior: smooth; }

/* Where a jump lands. The scroller offsets every target by the top bar plus
   24px of air (section 2). A track section wants no air: the divider above
   it is the previous section's dashed bottom border, and the ask is that
   this dashed line sits directly under the top bar.

   The top bar is --nav-h plus its own 1.5px bottom rule; the dashed divider
   is 1.2px, so the section's top edge lands about 3px below --nav-h, not
   24px. This rule is the fallback for a visit with JavaScript off. With it
   on, theme.js measures the jump instead, because browsers snap those two
   widths to device pixels and a fixed number is a pixel out somewhere. */
.section--target { scroll-margin-top: calc(3px - 24px); }

/* Signed-in editors see the WordPress admin bar. It is fixed from 601px up,
   and without this the sticky top bar slid underneath it, which also threw
   every jump off by the admin bar's height. Below 601px WordPress makes the
   bar scroll away with the page, so there is nothing to clear. The variable
   is WordPress's own and follows its 32px / 46px switch. */
@media screen and (min-width: 601px) {
	html:has(> body.admin-bar) { --bar-top: var(--wp-admin--admin-bar--height, 32px); }
}

/* A jump moves focus to the section so the keyboard continues from there.
   The section is not a control, so it draws no ring, like <main> (section 2). */
.section--target:focus { outline: none; }

/* ==========================================================================
   32. A real image beside the copy (text-visual, 0.10.28)
   No frame: the images made for this site carry the page colour and their
   own drawn lines, so a border would draw a box around a box.
   ========================================================================== */
.text-visual__media { min-width: 0; }
.text-visual__img { display: block; width: 100%; height: auto; }

/* Product-shot screenshots (client call, 28-09-2026): a real image used to
   sit inside the grid gutter like the copy beside it. Client wanted it
   reaching the slab's own edge instead — that's what "the container edge"
   means here, the vertical hairline .slab draws down each side (section 3)
   — which also frees enough width to show the shot noticeably larger, so
   both are done together rather than as two separate rules. Scoped to
   .text-visual__media specifically: that class only exists on the real-image
   branch of the block (render.php), never on the bandwidth or value-chain
   diagrams, so this can't reach anything but an actual product shot.
   Desktop only (mobile still stacks to one full-width column, section 2). */
@media (min-width: 861px) {
	.grid2:has(> .text-visual__media) { grid-template-columns: minmax(0, 1fr) minmax(0, 1.4fr); }
	.grid2:has(> .text-visual__media:first-child) { grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr); }
	.text-visual__media:last-child { margin-right: calc(var(--gutter) * -1); }
	.text-visual__media:first-child { margin-left: calc(var(--gutter) * -1); }
}

/* ==========================================================================
   33. Images in place of placeholders (0.10.29)
   skuridat_image_or_slot() prints .media where an image has been chosen and
   .slot where not. The image keeps its own proportions: slot heights are for
   the empty state only. No frame, as in section 32.
   ========================================================================== */
.media { min-width: 0; }
.media__img { display: block; width: 100%; height: auto; }

/* The hero figure keeps the slot's column width; its height follows the
   picture. */
.media.hero__figure-slot { height: auto; }

/* Founder portrait chips on a card: the slot's 72px square, cropped. */
.portrait--chip { width: 72px; height: 72px; flex: none; }

/* ==========================================================================
   34. The callback modal (0.10.30)
   Same dialog chrome as section 14 (.modal, .modal__dialog, .modal__head,
   .modal__close, .form-panel, .field-consent, .form-error, .form-success):
   only the body differs, one column instead of two (no scope box), with
   visible field labels and chip-style day/time pickers instead of
   placeholder-as-label text fields. Spec: 02 Content/terugbelverzoek.html.
   ========================================================================== */

.modal--callback .modal__dialog { width: min(560px, 100%); }
.modal--callback .modal__body { grid-template-columns: 1fr; padding: 32px; }

@media (max-width: 860px) {
	.modal--callback .modal__body { padding: 20px var(--gutter) 40px; }
}

.label {
	display: block;
	margin-bottom: 8px;
	font: 500 14px/1.4 var(--font-body);
	color: var(--heading);
}
.field-req,
.field-opt { color: var(--muted); font-weight: 400; }

fieldset { margin: 0; padding: 0; border: 0; min-width: 0; }
legend.label { padding: 0; }

.row2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }

.hint {
	margin: 0;
	font-size: 13px;
	line-height: 1.45;
	color: var(--muted);
}

.form-note {
	margin: 0;
	font-size: 13px;
	color: var(--muted);
	text-align: center;
}

@media (max-width: 600px) {
	.row2 { grid-template-columns: 1fr; }
}

/* ==========================================================================
   35. Gradient overlay and parallax on chosen images (0.10.33)
   Both are opt-in per image, set in the block editor. The overlay is a
   plain absolutely-positioned span painted over the image. Parallax is the
   classic fixed-background trick: PHP (skuridat_parallax_attr()) puts the
   image on the wrapper itself as an inline `background-image`, and this
   pins it with `background-attachment: fixed` so the image stays put in the
   viewport while the wrapper scrolls past it. The real <img> stays in the
   markup for alt text and SEO, made invisible so the background shows
   through instead.
   ========================================================================== */
.media,
.text-visual__media,
.article-figure__visual {
	position: relative;
	overflow: hidden;
}
.media__overlay {
	position: absolute;
	inset: 0;
	pointer-events: none;
}
[data-parallax] {
	background-position: center;
	background-size: cover;
	background-attachment: fixed;
}
[data-parallax] > img,
[data-parallax] > .media__img,
[data-parallax] > .text-visual__img,
[data-parallax] > .article-figure__img {
	opacity: 0;
}
/* iOS Safari has never supported background-attachment: fixed (it silently
   falls back to scroll), and a "fixed" image is a bigger repaint cost on any
   phone for an effect that barely reads on a screen that size — so this
   switches back to a normal image below the tablet breakpoint used
   elsewhere (skuridat_image_or_slot()'s `sizes` attribute), same as
   reduced-motion. */
@media (max-width: 860px), (prefers-reduced-motion: reduce) {
	[data-parallax] {
		background-image: none !important;
		background-attachment: scroll;
	}
	[data-parallax] > img,
	[data-parallax] > .media__img,
	[data-parallax] > .text-visual__img,
	[data-parallax] > .article-figure__img {
		opacity: 1;
	}
}
.portrait--chip img { width: 100%; height: 100%; object-fit: cover; border-radius: var(--radius); display: block; }

/* ==========================================================================
   36. Operating model (0.10.36)
   A radial SVG instrument (drawn in PHP, blocks/operating-model/render.php)
   beside its copy and legend. Four grid children, always in this DOM order
   — .om__text, .om__figure, .om__domains, .om__legend — placed by
   grid-template-areas per breakpoint rather than reordered in markup.
   .om__domains is the phone-only HTML restatement of the SVG's own domain
   labels, which get too small to read below 720px (see .om-hide-narrow).
   ========================================================================== */
.om {
	display: grid;
	grid-template-columns: 1fr;
	grid-template-areas: "text" "figure" "domains" "legend";
	row-gap: 40px;
}
.om__domains { display: none; }
.om__figure { min-width: 0; }
.om__figure svg { display: block; width: 100%; height: auto; }

@media (max-width: 719px) {
	.om__domains {
		display: grid;
		grid-template-columns: 1fr 1fr;
		gap: 12px;
		font: 400 10px/1.3 var(--font-mono);
		letter-spacing: .1em;
		text-transform: uppercase;
		color: var(--body);
	}
	.om__domain { display: flex; align-items: center; gap: 8px; }
	.om__domain::before {
		content: "";
		width: 3px;
		height: 12px;
		flex: none;
		background: var(--body);
		opacity: .45;
	}
	.om__domain--accent::before { background: var(--accent); opacity: 1; }
	.om-hide-narrow { display: none; }
}

@media (min-width: 720px) {
	.om {
		grid-template-columns: 1fr;
		grid-template-areas: "figure" "text" "legend";
		row-gap: 40px;
	}
	.om__figure { max-width: 720px; margin-inline: auto; }
}

@media (min-width: 1200px) {
	.om {
		grid-template-columns: 720px minmax(0, 1fr);
		grid-template-areas: "figure text" "figure legend";
		column-gap: 64px;
		row-gap: 40px;
		align-items: center;
	}
	.om__figure { max-width: none; margin-inline: 0; padding-block: 24px; align-self: center; }
}

.om__text { grid-area: text; }
.om__text > *:last-child { margin-bottom: 0; }
.om__figure { grid-area: figure; }
.om__domains { grid-area: domains; }
.om__legend { grid-area: legend; }

/* The legend, same row-and-swatch shape as the mobile menu's language list
   (section 34) but with its own colours — three fixed rows, one per band
   the drawing shows, not a repeatable list. */
.om__legend {
	padding-top: 32px;
	border-top: var(--line-foot);
	display: grid;
	gap: 16px;
}
.om__legend-row {
	display: grid;
	grid-template-columns: 24px minmax(0, 1fr);
	gap: 14px;
}
.om__swatch {
	display: block;
	width: 3px;
	height: 18px;
	margin: 1px 0 0 10px;
	font-style: normal;
}
.om__swatch--0 { background: var(--body); opacity: .8; }
.om__swatch--1 { background: var(--body); opacity: .45; }
.om__swatch--2 { background: var(--accent); }
.om__legend-name {
	margin: 0 0 5px;
	font: 500 10px/1.2 var(--font-mono);
	letter-spacing: .1em;
	text-transform: uppercase;
	color: var(--heading);
}
.om__legend-name--accent { color: var(--accent); }
.om__legend-desc { margin: 0; font: 400 14px/1.5 var(--font-body); color: var(--body); }

/* The drawing's own type, set on SVG <text> so it scales and stays crisp
   with the rest of the artwork rather than being an HTML overlay. */
.om-label {
	font: 400 10.5px/1 var(--font-mono);
	letter-spacing: 1.3px;
	text-transform: uppercase;
	fill: var(--body);
}
.om-label--accent { fill: var(--accent); }
.om-core-label { font: 600 15px/1 var(--font-display); fill: var(--heading); }

/* Motion: the specialist ring breathes, the senior tips only surface at
   each domain's peak. Paused until the figure scrolls into view (theme.js
   adds .is-playing via IntersectionObserver), and off entirely under
   reduced motion — where the ticks sit at full length with their tips
   showing, which is the correct still, not a broken animation. */
.om-tick-ai,
.om-tick-senior {
	transform-box: fill-box;
	transform-origin: 50% 100%;
}
.om-tick-ai { animation: om-ai var(--dur) ease-in-out var(--delay) infinite; animation-play-state: paused; }
.om-tick-senior { animation: om-senior var(--dur) ease-in-out var(--delay) infinite; animation-play-state: paused; }
.om__figure.is-playing .om-tick-ai,
.om__figure.is-playing .om-tick-senior {
	animation-play-state: running;
}
@keyframes om-ai { 0%, 100% { transform: scaleY(.4); } 50% { transform: scaleY(1); } }
@keyframes om-senior { 0%, 38%, 62%, 100% { transform: scaleY(0); } 50% { transform: scaleY(1); } }

@media (prefers-reduced-motion: reduce) {
	.om-tick-ai,
	.om-tick-senior {
		animation: none;
	}
}

/* ==========================================================================
   36b. How-we-work diagrams (0.11.16) — three-col-detail's "operating-radial"
   and "eu-stack-static" column visuals, About page design turn 31a/31b.
   Both cap at 500px and sit left-aligned in a 640px text column, same
   treatment as the diamond shape above. The radial reuses .om__figure (so
   theme.js's existing IntersectionObserver play/pause picks it up for
   free) and the legend row/swatch/name/desc atoms from section 36, but
   with this instance's own spacing per the handoff, not the homepage
   block's 32px/16px rhythm.
   ========================================================================== */
.detail-col__diagram { max-width: 500px; margin-top: 24px; }

/* Client feedback: the two columns' diagrams needed roughly the same
   vertical footprint, and the two gaps were pulling in opposite
   directions — the radial's own drawing already carries generous empty
   space below its ring before the domain label, so the 24px gap to its
   key on top of that read as too much air; the static stack sits flush
   under its paragraph with no such built-in space, so the same 24px
   there read as too little. Adjusted per instance rather than changing
   the shared 24px baseline above. */
.detail-col__diagram--stack { margin-top: 48px; }

.detail-col__diagram-key {
	/* Three columns side by side, per the handoff — unlike the homepage's
	   own operating-model legend, which stacks its three rows in one
	   column. Missed on the first pass: the grid had no explicit column
	   count, so it fell back to one column and ran the three rows down
	   the page instead of across it. */
	margin-top: 8px;
	padding-top: 24px;
	border-top: var(--line-foot);
	display: grid;
	grid-template-columns: repeat( 3, minmax( 0, 1fr ) );
	gap: 24px;
}
@media ( max-width: 479px ) {
	/* Three columns at 500px cap width is workable; at phone width the
	   swatch+label+description no longer fits three across. */
	.detail-col__diagram-key { grid-template-columns: 1fr; }
}

/* 31b's static stack: plain HTML, no SVG, everything at full opacity — a
   different, simpler component from the animated <sk-eu-stack> walkthrough
   (see skuridat_how_we_work_stack()'s docblock for why). Marked up as
   nested <ol>s; list styling reset since the numbering isn't wanted here. */
.hww-stack,
.hww-stack__eea-list {
	list-style: none;
	margin: 0;
	padding: 0;
	display: flex;
	flex-direction: column;
	gap: 12px;
}
.hww-stack__box {
	display: flex;
	align-items: center;
	min-height: 44px;
	box-sizing: border-box;
	padding: 12px;
	border: 1.2px solid rgba(168, 203, 207, .45);
	background: var(--page);
	font: 500 12px/1.3 var(--font-mono);
	letter-spacing: .06em;
	text-transform: uppercase;
	color: var(--heading);
}
.hww-stack__row--sources,
.hww-stack__row--platform,
.hww-stack__row--tenants {
	display: grid;
	grid-template-columns: repeat(3, minmax(0, 1fr));
	gap: 8px;
}
.hww-stack__note {
	text-align: center;
	font: 500 11px var(--font-mono);
	letter-spacing: .1em;
	text-transform: uppercase;
	color: var(--muted);
}
.hww-stack__eea {
	position: relative;
	padding: 16px;
	border: 1.4px dashed rgba(168, 203, 207, .7);
}
.hww-stack__box--dim { opacity: .5; }
.hww-stack__box--accent {
	border-color: var(--accent);
	background: rgba(252, 86, 29, .08);
}
.hww-stack__wide {
	display: flex;
	justify-content: space-between;
	flex-wrap: wrap;
	gap: 12px;
	padding: 12px;
	font: 500 12px/1.3 var(--font-mono);
	letter-spacing: .06em;
	text-transform: uppercase;
	color: var(--heading);
}
.hww-stack__wide--exchange {
	border: 1.2px dashed rgba(168, 203, 207, .8);
	background: rgba(168, 203, 207, .08);
}
.hww-stack__wide--supplier {
	border: 1.2px solid rgba(242, 242, 240, .7);
	background: rgba(242, 242, 240, .07);
}
.hww-stack__muted { color: var(--body); }
.hww-stack__box--tenant {
	border-color: rgba(168, 203, 207, .7);
	background: #10252a;
}
.hww-stack__box--tenant-strong {
	border-color: rgba(242, 242, 240, .8);
	background: #10252a;
}
.hww-stack__infra {
	margin-top: 4px;
	padding-top: 12px;
	border-top: 1.2px solid rgba(168, 203, 207, .25);
	font: 500 12px/1.3 var(--font-mono);
	letter-spacing: .06em;
	text-transform: uppercase;
	color: var(--body);
}
.hww-stack__boundary {
	/* The generic `p { max-width: var(--measure) }` reading-width cap
	   (section 5) was capping this narrower than the diagram above it,
	   so text-align: right lined its two lines up short of the diagram's
	   own right edge instead of flush with it. This caption isn't prose,
	   it's a label for the box above, so it gets the box's own width
	   back. */
	max-width: none;
	margin: 8px 0 0;
	text-align: right;
	font: 500 12px/1.5 var(--font-mono);
	letter-spacing: .06em;
	text-transform: uppercase;
	color: var(--heading);
}
.hww-stack__boundary span { color: var(--body); }

@media (max-width: 639px) {
	.detail-col__diagram { max-width: none; }
}

/* ==========================================================================
   37. News ticker (0.10.38, adjusted 0.10.39, embeddable 0.10.40)
   Ported from the Skuridat Ticker design prototype: one to three rows of
   logo/label chips, each row a doubled item list sliding under its own
   direction, JS-driven (theme.js) rather than CSS keyframes, because the
   loop point depends on each track's own rendered width. The rows print
   inside .wrap (0.10.39: moved back in from a slab-edge full bleed, per
   client request — aligned to the content column now), so the fade mask
   reads against the gutter, not the slab edge.

   0.10.38 dropped the intro copy (client wanted rows only) and swapped the
   text "code" chip for an uploaded square logo, rendered through
   skuridat_image_or_slot() like every other image in the theme — an item
   with no logo chosen yet gets the theme's own labelled placeholder
   (.slot), not a blank box.

   0.10.40: the markup here (.ticker and everything under it) is printed by
   the shared skuridat_ticker_rows() (inc/template-tags.php), which
   three-col-detail's media list also calls for a "ticker" entry — this CSS
   applies wherever that markup ends up, not just inside the news-ticker
   block's own .section.
   ========================================================================== */
.ticker {
	display: flex;
	flex-direction: column;
	gap: 28px;
	-webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
	mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
}
.ticker__row { overflow: hidden; }
.ticker__track {
	display: flex;
	width: max-content;
	will-change: transform;
}
.ticker__item {
	display: flex;
	align-items: center;
	gap: 14px;
	padding-right: 56px;
	flex: none;
}
/* Every logo shows at the same 32x32, whatever size or shape was uploaded
   — object-fit: cover on the image, not the slot's own diagonal-stripe
   placeholder, which stays as designed at this smaller size. */
.ticker__logo.slot,
.ticker__logo.media {
	width: 32px;
	height: 32px;
	flex: none;
}
.ticker__logo.slot { font-size: 10px; }
.ticker__logo .media__img {
	width: 100%;
	height: 100%;
	object-fit: cover;
}
.ticker__label {
	font: 500 16px/1.2 var(--font-display);
	color: var(--heading);
	white-space: nowrap;
}

@media (max-width: 600px) {
	.ticker__label { font-size: 14px; }
	.ticker__item { padding-right: 36px; gap: 10px; }
}

/* Reduced motion: theme.js never starts the scroll loop, so every track
   sits at its resting transform (0) — the first copy of each row, fully
   readable, not a stalled animation. */

/* ==========================================================================
   38. Platform hero — CTEM walkthrough (0.10.42)
   Copy for the <ctem-hospital> custom element (assets/js/ctem-hospital.js),
   which is fully self-styled inline by its own script — this only lays out
   the eyebrow/heading/lede above it and the 48px gap down to the element.
   Fixed padding (96px top / 56px bottom), not the site's clamped --sec-pad:
   this is the one hero built to an exact desktop drawing, not the general
   section rhythm, and only desktop is designed (see the handoff's own
   "Responsive" section).
   ========================================================================== */
.ctem-hero { padding-block: 96px 56px; }

.ctem-hero__heading {
	margin: 0 0 16px;
	font: 600 48px/1.1 var(--font-display);
	color: var(--heading);
	max-width: 18em;
}
.ctem-hero__lede {
	margin: 0;
	max-width: 34em;
	color: var(--body);
}
/* render.php now runs the lede through skuridat_paragraphs(), so a blank
   line in the field prints a second <p class="ctem-hero__lede">. The
   class's own margin:0 (needed so the first paragraph sits flush under
   the heading) outranks the sitewide `p + p` rule by specificity, so a
   second paragraph needs its own top margin spelled out here. */
.ctem-hero__lede + .ctem-hero__lede { margin-top: 16px; }

/* The link moved into the stage's own top-right corner (client feedback)
   — a plain sibling of <ctem-hospital>, positioned over it with CSS
   rather than injected into the custom element's own markup, so that
   file stays exactly the handoff's. Mirrors the caption box
   ctem-hospital.js already draws top-left, at the same offsets, for
   full (≥1024) and compact (640–1023): 24/22px and 18/16px respectively.
   Below 640 the stage drops to its own much smaller mobile layout with
   no room for a floating corner button, so it flows as a plain block
   under the stage instead of overlapping it — still visible and
   functional, just not perched on the corner. */
.ctem-hero__stage-wrap { position: relative; }
.ctem-hero__stage-cta { margin-top: 12px; }
.ctem-hero__stage-cta .row { margin-top: 0 !important; }
@media (min-width: 640px) {
	.ctem-hero__stage-cta {
		position: absolute;
		top: 16px;
		right: 18px;
		margin-top: 0;
		z-index: 5;
	}
	/* Cancels the sitewide mobile .btn{width:100%} rule (style.css, up to
	   860px) for the 640–860px slice where this is now positioned
	   absolutely: a full-width button there would stretch across the
	   whole stage. Below 640px the button is a plain block under the
	   stage (not positioned), so it keeps the sitewide full-width mobile
	   button convention instead. */
	.ctem-hero__stage-cta .btn { width: auto; }
}
@media (min-width: 1024px) {
	.ctem-hero__stage-cta { top: 22px; right: 24px; }
}

.ctem-hero__stage-wrap { margin-top: 48px; }

/* ==========================================================================
   39. Phase artifact — "Sweep" (design 26a, 0.10.49)
   Wraps the <ctem-scoping> element (assets/js/ctem-scoping.js) so its 16:10
   SVG scales with the column it sits in — the element's own inline sizing
   is display:block only (see that file's header), so plain width/height
   here is enough, no !important needed. phase-section already gives it a
   min-width:0 column via .grid2, so it can shrink below the SVG's own
   pixel dimensions without overflowing.
   ========================================================================== */
.phase-artifact { min-width: 0; }
.phase-artifact ctem-scoping,
.phase-artifact ctem-discovery,
.phase-artifact ctem-prioritization,
.phase-artifact ctem-validation,
.phase-artifact ctem-mobilization,
.phase-artifact svg {
	display: block;
	width: 100%;
	height: auto;
}

/* ==========================================================================
   40. Value chain figure (design 21c, 0.10.52)
   Lives inside text-visual as the "value-chain" visual, replacing the
   generic slot/image/bandwidth cell there. Unlike those, its own visual
   column isn't half the row — the design wants a fixed 820px figure+steps
   column beside a flexible copy column, so .grid2--chain overrides
   .grid2--wide's equal split just for this instance; the 64px gap itself
   is unchanged, since --gap-2wide already clamps to exactly that.

   The whole animation is CSS keyframes on plain SVG/HTML elements, driven
   by one 12s loop — no JS tick loop like the CTEM phase artifacts, since
   nothing here needs per-frame layout math.

   Two different "not animating" states, on purpose:
   - Not yet visible: animation-play-state starts paused (theme.js's
     shared pausedFigures observer un-pauses it, same as the operating
     model and bandwidth diagram), which freezes each element at its own
     keyframe's 0% frame — undrawn path, unlit nodes — the ordinary start
     of the reveal, same as those two figures at rest.
   - prefers-reduced-motion: animation is removed outright (`!important`,
     below), which instead falls back to each element's own un-animated
     base CSS value. Those bases are deliberately written as the *lit*
     end state — path drawn, nodes and square lit, rows at full opacity,
     pulse invisible — matching the handoff's reduced-motion spec exactly,
     with no separate override rule needed.
   ========================================================================== */

.grid2--chain { grid-template-columns: minmax(0, 1fr) 820px; }

.value-chain {
	display: grid;
	grid-template-columns: 400px minmax(0, 1fr);
	gap: 40px;
	min-width: 0;
}

.value-chain__figure { min-width: 0; }
.value-chain__svg { display: block; width: 100%; height: auto; overflow: visible; }
.value-chain__caption {
	margin: 16px 0 0;
	text-align: center;
	font: 500 10px/1.2 var(--font-mono);
	letter-spacing: .1em;
	text-transform: uppercase;
	color: var(--muted);
}

.vc-ring { fill: none; stroke: var(--body); stroke-opacity: .35; stroke-width: 1.2px; stroke-dasharray: 4 6; }
.vc-base-line { stroke: var(--body); stroke-opacity: .22; stroke-width: 1.2px; }
.vc-node { fill: var(--page); stroke: var(--body); stroke-width: 1.4px; }
.vc-node-source { fill: var(--accent); stroke: var(--accent); stroke-width: 1.4px; }
.vc-square-base { fill: var(--page); stroke: var(--body); stroke-width: 1.4px; }

/* The fade-out used to live on the wrapping <g> (a separate vc-g
   animation on .vc-group), with each line's own draw-in on vc-s1/s2/s3 —
   two independent animations on two different elements, timed to match
   on paper but never guaranteed to reset in the same frame, which showed
   up as a brief orange flash right at the loop seam (client-reported).
   Folding the fade into each line's own vc-s1/s2/s3 keyframe instead
   means one animation, one element, one timeline — dashoffset and
   opacity can't drift apart because they're never on separate clocks. */
.vc-hop { stroke: var(--accent); stroke-width: 1.8px; stroke-dashoffset: 0; }
.vc-hop1 { stroke-dasharray: 117.2; animation: vc-s1 12s ease-in-out infinite; }
.vc-hop2 { stroke-dasharray: 74.4; animation: vc-s2 12s ease-in-out infinite; }
.vc-hop3 { stroke-dasharray: 60.1; animation: vc-s3 12s ease-in-out infinite; }

.vc-node-lit { fill: var(--accent); stroke: var(--accent); stroke-width: 1.4px; opacity: 1; }
.vc-node2 { animation: vc-n2 12s linear infinite; }
.vc-node3 { animation: vc-n3 12s linear infinite; }

.vc-pulse {
	fill: none;
	stroke: var(--accent);
	stroke-width: 1.2px;
	opacity: 0;
	transform-box: fill-box;
	transform-origin: 50% 50%;
	animation: vc-p 2.4s ease-out infinite;
}

.vc-square { fill: rgba(252, 86, 29, .12); stroke: var(--accent); stroke-width: 1.6px; opacity: 1; animation: vc-n4 12s linear infinite; }

.vc-label { font: 500 10.5px/1 var(--font-mono); letter-spacing: 1.3px; fill: var(--body); }
.vc-label--accent { fill: var(--accent); }

/* Starts paused, like the operating model and bandwidth diagram above it
   in this file — theme.js's shared pausedFigures observer adds
   .is-playing once the figure is actually on screen (threshold .2, close
   enough to the handoff's own .3), so a copy scrolled straight past never
   spends a frame animating off-screen. */
.vc-anim { animation-play-state: paused; }
.value-chain.is-playing .vc-anim { animation-play-state: running; }

@keyframes vc-s1 { 0%, 6% { stroke-dashoffset: 117.2; opacity: 1; } 16%, 90% { stroke-dashoffset: 0; opacity: 1; } 97%, 100% { stroke-dashoffset: 0; opacity: 0; } }
@keyframes vc-s2 { 0%, 28% { stroke-dashoffset: 74.4; opacity: 1; } 38%, 90% { stroke-dashoffset: 0; opacity: 1; } 97%, 100% { stroke-dashoffset: 0; opacity: 0; } }
@keyframes vc-s3 { 0%, 50% { stroke-dashoffset: 60.1; opacity: 1; } 60%, 90% { stroke-dashoffset: 0; opacity: 1; } 97%, 100% { stroke-dashoffset: 0; opacity: 0; } }
@keyframes vc-n2 { 0%, 15% { opacity: 0; } 17%, 90% { opacity: 1; } 97%, 100% { opacity: 0; } }
@keyframes vc-n3 { 0%, 37% { opacity: 0; } 39%, 90% { opacity: 1; } 97%, 100% { opacity: 0; } }
@keyframes vc-n4 { 0%, 59% { opacity: 0; } 61%, 92% { opacity: 1; } 98%, 100% { opacity: 0; } }
@keyframes vc-r1 { 0%, 16% { opacity: 1; } 20%, 90% { opacity: .7; } 97%, 100% { opacity: 1; } }
@keyframes vc-r2 { 0%, 14% { opacity: .5; } 18%, 38% { opacity: 1; } 42%, 90% { opacity: .7; } 97%, 100% { opacity: .5; } }
@keyframes vc-r3 { 0%, 36% { opacity: .5; } 40%, 60% { opacity: 1; } 64%, 90% { opacity: .7; } 97%, 100% { opacity: .5; } }
@keyframes vc-r4 { 0%, 58% { opacity: .5; } 62%, 92% { opacity: 1; } 98%, 100% { opacity: .5; } }
@keyframes vc-p { 0% { transform: scale(1); opacity: .8; } 70%, 100% { transform: scale(3.2); opacity: 0; } }

/* Step titles light up in the same windows their row is "active" in
   above (vc-r1–vc-r4) — reusing those exact percentages is what keeps
   the title colour, the row opacity and the path/node lighting reading
   as one event instead of three approximately-synced ones. */
@keyframes vc-t1 { 0%, 16% { color: var(--accent); } 20%, 100% { color: var(--heading); } }
@keyframes vc-t2 { 0%, 14% { color: var(--heading); } 18%, 38% { color: var(--accent); } 42%, 100% { color: var(--heading); } }
@keyframes vc-t3 { 0%, 36% { color: var(--heading); } 40%, 60% { color: var(--accent); } 64%, 100% { color: var(--heading); } }
@keyframes vc-t4 { 0%, 58% { color: var(--heading); } 62%, 92% { color: var(--accent); } 98%, 100% { color: var(--heading); } }

/* The row numbers, same windows again, off-colour --body instead of
   --heading. */
@keyframes vc-tn1 { 0%, 16% { color: var(--accent); } 20%, 100% { color: var(--body); } }
@keyframes vc-tn2 { 0%, 14% { color: var(--body); } 18%, 38% { color: var(--accent); } 42%, 100% { color: var(--body); } }
@keyframes vc-tn3 { 0%, 36% { color: var(--body); } 40%, 60% { color: var(--accent); } 64%, 100% { color: var(--body); } }
@keyframes vc-tn4 { 0%, 58% { color: var(--body); } 62%, 92% { color: var(--accent); } 98%, 100% { color: var(--body); } }

.value-chain__steps { min-width: 0; }
.value-chain__list { list-style: none; margin: 0; padding: 0; }

.value-chain__row {
	display: grid;
	grid-template-columns: 40px minmax(0, 1fr);
	gap: 16px;
	padding: 18px 0;
	border-top: 1.2px solid rgba(168, 203, 207, .2);
	opacity: 1;
}
/* Row 01 is active for the first 16% of the loop, same as its node/hop —
   see vc-r1 above, which starts at opacity 1 rather than the .5 the other
   three rows' "pending" state uses. */
.value-chain__row:first-child { border-top: none; }

.value-chain__num {
	font: 500 16px/22px var(--font-mono);
	color: var(--body);
	animation-play-state: paused;
}
.value-chain.is-playing .value-chain__num { animation-play-state: running; }
/* Same "current step" highlight as the title beside it (below), on the
   same vc-r1–vc-r4 timing — client feedback: the number should light and
   dim with the title, not just the title. Off-colour is --body here
   rather than the title's --heading, matching this element's own resting
   colour, so these get their own keyframes instead of reusing vc-t1–vc-t4. */
.vc-row1 .value-chain__num { animation-name: vc-tn1; animation-duration: 12s; animation-timing-function: linear; animation-iteration-count: infinite; }
.vc-row2 .value-chain__num { animation-name: vc-tn2; animation-duration: 12s; animation-timing-function: linear; animation-iteration-count: infinite; }
.vc-row3 .value-chain__num { animation-name: vc-tn3; animation-duration: 12s; animation-timing-function: linear; animation-iteration-count: infinite; }
.vc-row4 .value-chain__num { animation-name: vc-tn4; animation-duration: 12s; animation-timing-function: linear; animation-iteration-count: infinite; }

.value-chain__tier {
	display: block;
	margin-bottom: 6px;
	font: 500 10px/1.2 var(--font-mono);
	letter-spacing: .1em;
	text-transform: uppercase;
	color: var(--muted);
}
.value-chain__title {
	display: block;
	font: 600 18px/1.3 var(--font-display);
	letter-spacing: -.01em;
	color: var(--heading);
	animation-play-state: paused;
}
.value-chain.is-playing .value-chain__title { animation-play-state: running; }
/* Which step is "current" lights that row's title instead of a fixed
   row staying accent — client feedback: the highlight should follow the
   path (1 lights, then fades as 2 lights, and so on through the centre
   square), not sit permanently on one row. Same vc-r1–vc-r4 windows as
   the row opacity above, so title colour and row opacity always agree
   on which step is active. */
/* Longhand animation-name (not the animation: shorthand) on purpose: the
   shorthand resets every sub-property it doesn't mention back to its
   initial value, including animation-play-state — which would silently
   flip these back to "running" and defeat the paused/is-playing rule
   above, higher specificity or not. */
.vc-row1 .value-chain__title { animation-name: vc-t1; animation-duration: 12s; animation-timing-function: linear; animation-iteration-count: infinite; }
.vc-row2 .value-chain__title { animation-name: vc-t2; animation-duration: 12s; animation-timing-function: linear; animation-iteration-count: infinite; }
.vc-row3 .value-chain__title { animation-name: vc-t3; animation-duration: 12s; animation-timing-function: linear; animation-iteration-count: infinite; }
.vc-row4 .value-chain__title { animation-name: vc-t4; animation-duration: 12s; animation-timing-function: linear; animation-iteration-count: infinite; }
.value-chain__detail { margin: 4px 0 0; font: 400 14px/1.5 system-ui, sans-serif; color: var(--body); }

.value-chain__foot {
	display: flex;
	flex-wrap: wrap;
	gap: 28px;
	padding-top: 18px;
	border-top: var(--line-section);
	font: 500 10px/1.2 var(--font-mono);
	letter-spacing: .1em;
	text-transform: uppercase;
	color: var(--muted);
}

@media (prefers-reduced-motion: reduce) {
	.vc-anim, .value-chain__title, .value-chain__num { animation: none !important; }
	.vc-pulse { display: none; }
}

@media (max-width: 1200px) {
	.grid2--chain { grid-template-columns: 1fr; }
}

@media (max-width: 760px) {
	.value-chain { grid-template-columns: 1fr; }
	.value-chain__figure { max-width: 400px; }
}

/* ==========================================================================
   41. Homepage hero — "Out of the dark" (design 27e/27f, 0.10.58)
   The block's own hero.css ships as-is (blocks/skuridat-hero/hero.css,
   loaded through block.json's own "style" field) — everything here is
   theme-specific glue, not a change to that file.

   The handoff's --skh-header default (and its README's own suggested
   80px/64px breakpoint pair) is a reasonable guess at a header height it
   has no way to know; this theme's real sticky header is already fluid
   (--nav-h, clamped 56–80px) and already accounts for the admin bar
   (--bar-top), so mapping straight to those existing tokens is more
   accurate than picking new fixed numbers that would drift out of sync
   with the header the moment either one's own clamp() changes.
   ========================================================================== */
.skh { --skh-header: calc(var(--nav-h) + var(--bar-top)); }

/* A section divider after the hero, on the homepage: whatever the next
   block is (currently the bandwidth diagram's "Compliance is the floor,
   not the target."), this theme's own sections separate with a dashed
   --line-section, but this hero doesn't run through skuridat_section_open()
   at all — it's the handoff's own <section>, so there was no line here
   yet. Client asked for a solid one instead of the usual dashed rule
   anyway, to match the solid 1.3px frame the hero already draws around
   its own stage and phase strip (hero.css), rather than introducing a
   third line weight/style to the page.

   Flush against the hero's own last row on purpose, no padding-bottom:
   an earlier pass added --sec-pad worth of space before the line, same
   rhythm as .section's own padding-block, but the client wants the line
   exactly where the hero's dark background ends and the next section's
   own (lighter-toned) background starts — not a gap of hero-coloured
   background first. */
.skh { border-bottom: 1.3px solid rgba(168, 203, 207, .4); }
