@charset "UTF-8";

/* ==========================================================================
   SIMSAN Engineering
   assets/css/site.css

   Contrast ratios quoted in comments are measured, not estimated.
   Every colour, size, easing and duration below is a token. No component
   hard-codes a brand value.

     1.  Fonts
     2.  Design tokens
     3.  Field context classes
     4.  Reset and base
     5.  Layout primitives
     6.  Typography
     7.  Buttons and links
     8.  Motion system
     9.  Logo intro
     10. Header and navigation
     11. Footer
     12. Inquiry overlay and forms
     13. Technical components
     14. Home sections
     15. Services sections
     16. About sections
     17. Plain PHP fallback page
     18. Reduced motion
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. Fonts
   Self hosted, served from this origin. The site makes no runtime request to
   Google Fonts, Fontshare or any other third party host.
   Licences: assets/fonts/licenses.txt
   -------------------------------------------------------------------------- */

@font-face {
  font-family: "Quantify";
  src: url("../fonts/quantify.woff2") format("woff2");
  /* Measured: static, single weight (usWeightClass 400), no fvar/STAT axis,
     no bold master. Declaring a range here would trigger synthetic bolding.
     font-synthesis: none is declared on html (Sec 4, Reset and base) so no
     component can silently request a fake weight/oblique again. */
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Switzer";
  /* format("woff2-variations") is not valid in Firefox/Safari -- a plain
     woff2 mime still resolves the variable axes (fvar/STAT are read from
     the font data itself, not from this hint). */
  src: url("../fonts/switzer-variable.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

/* No monospaced face ships with this site: the previous terminal-style
   mono face and both its weights are removed entirely (creative-direction-v2
   Section 11.1). */

/* --------------------------------------------------------------------------
   2. Design tokens
   -------------------------------------------------------------------------- */

:root {
  /* --- Raw brand. Never referenced directly by a component. -------------- */
  --brand-ink: #1c1c28;
  --brand-navy: #062045;
  --brand-cyan: #1ec1cb;
  --brand-aqua: #98deeb;
  --brand-white: #ffffff;

  /* Law 1, the Cyan Law: #1ec1cb on #98deeb is 1.46:1 and on #ffffff is
     2.20:1 -- both fail even the 3:1 non-text floor. Cyan therefore never
     carries meaning on a light ground. This hue-preserving deep variant
     (5.12:1 on aqua, 7.69:1 on white) carries the accent role there instead. */
  --brand-cyan-deep: #0e5c63;
  --brand-cyan-lift: #45d3dc;
  --brand-ink-lift: #23232f;

  /* --- Fields: the two page grounds. Law 2: #062045 measures 1.04:1 against
     #1c1c28 -- effectively the same colour -- so navy is never a dark-zone
     surface. It is retained only as the ink of the light zone (below). --- */
  --field-ink: var(--brand-ink);
  --field-paper: var(--brand-aqua);

  /* --- Surfaces ------------------------------------------------------------
     Tonal steps only. None of these differs enough from its ground to serve
     as a boundary on its own; they are used for section rhythm, never to
     fake a focus ring or a card edge. BLOCKER B3 fix: the previous flat
     hexes here (--brand-ink-sink #14141d, --surface-paper-sunken #89cbda)
     were a ~10% value shift from their own ground, applied as full-bleed
     700-850px SECTION backgrounds (.section--sunken) -- close enough to
     read as a second brand tone, far enough to read as a rendering bug
     (sandwiched between two identical #98deeb fields on index). Both raw
     tokens are deleted; the only two grounds on the site are now #1c1c28
     and #98deeb (.section--sunken no longer changes the background at all
     -- see Sec 5, Layout primitives -- it adds a hairline seam instead).
     --surface-sunken survives only as an alpha wash for small UI surfaces
     (a placeholder card, a callout box) where a subtle tonal step is
     legible as intentional at that size, never as a page-scale ground. */
  --surface-base: var(--field-ink);
  --surface-raised: var(--brand-ink-lift);
  --surface-sunken: rgb(255 255 255 / 0.05);

  --surface-paper-base: var(--brand-aqua);
  --surface-paper-sunken: rgb(6 32 69 / 0.06);

  /* --- Text levels, measured against their own field (D11 / creative-direction-v2 Sec 3.2/3.3) */
  --text-ink-primary: #ffffff;        /* 16.85:1 */
  /* HIGH H1 fix: the old #b4bac6/#9aa2b2/#7f8799/#6f7789 ramp was a cool
     grey with no relationship to the brand -- the exact "SaaS clone" ink.
     Alpha-of-white keeps the hue locked to the ground (--field-ink
     #1c1c28) and introduces no foreign colour; .hero__lead already did
     this at 0.82. Ratios recomputed against #1c1c28 (not assumed): alpha
     compositing is not linear with a flat hex's ratio, so each step below
     is the actual measured contrast of the composited colour, not the old
     hex's number carried over. */
  --text-ink-secondary: rgb(255 255 255 / 0.72);  /* 9.23:1 (composite ~#bfbfc3) */
  --text-ink-muted: rgb(255 255 255 / 0.55);      /* 5.93:1 (composite ~#99999e) */
  --text-ink-faint: rgb(255 255 255 / 0.47);      /* 4.70:1, lowest legal step, still AA */
  --text-ink-disabled: rgb(255 255 255 / 0.40);   /* 3.78:1, disabled controls only (WCAG 1.4.3 exemption) */
  --text-ink-signal: var(--brand-cyan);   /* 7.66:1 -- cyan is legal as text on dark */
  --text-ink-aqua: var(--brand-aqua);     /* 11.22:1 */

  --text-paper-primary: #062045;      /* 10.75:1 */
  /* HIGH H1 fix, aqua side: alpha-of-navy replaces the hard-coded grey-blue
     hexes, same reasoning as the dark ramp above. Ratios recomputed against
     #98deeb. */
  --text-paper-secondary: rgb(6 32 69 / 0.88);  /* 8.11:1 */
  --text-paper-muted: rgb(6 32 69 / 0.72);      /* 5.23:1 */
  --text-paper-faint: rgb(6 32 69 / 0.67);      /* 4.55:1, large text only (>=24px), never body or labels */
  --text-paper-disabled: rgb(6 32 69 / 0.45);   /* 2.56:1, disabled controls only (WCAG 1.4.3 exemption) */
  /* Cyan is banned on aqua (1.46:1, fails the 3:1 non-text floor). The deep
     variant carries the accent hue instead, so the hue family stays continuous. */
  --text-paper-signal: var(--brand-cyan-deep); /* 5.12:1 */

  /* Active aliases. Each [data-field] block re-points these. */
  --text-primary: var(--text-ink-primary);
  --text-secondary: var(--text-ink-secondary);
  --text-muted: var(--text-ink-muted);
  --text-faint: var(--text-ink-faint);
  --text-signal: var(--text-ink-signal);
  --text-accent: var(--text-ink-aqua);

  /* --- Hairlines. Never a flat grey. Cyan never used as a line on aqua. -- */
  --line-ink-hair: rgb(255 255 255 / 0.10);
  --line-ink-soft: rgb(255 255 255 / 0.18);
  --line-ink-strong: rgb(255 255 255 / 0.30);
  --line-paper-hair: rgb(6 32 69 / 0.14);
  --line-paper-soft: rgb(6 32 69 / 0.24);
  --line-paper-strong: rgb(6 32 69 / 0.40);
  --line-signal: var(--brand-cyan);
  --line-signal-dim: rgb(30 193 203 / 0.38);

  --line-hair: var(--line-ink-hair);
  --line-soft: var(--line-ink-soft);
  --line-strong: var(--line-ink-strong);

  /* --- Signal and state. Non-cyan hues are licensed to form validation. --- */
  --signal: var(--brand-cyan);
  --signal-hover: var(--brand-cyan-lift);
  --signal-on-light: var(--brand-cyan-deep);
  --signal-ink: var(--brand-navy);     /* text on a cyan fill, 7.34:1 */
  --signal-wash: rgb(30 193 203 / 0.08);
  --signal-wash-paper: rgb(14 92 99 / 0.10);

  /* The primary control. One solid cyan fill with dark text passes on BOTH
     grounds (7.66:1 on dark ground, 7.34:1 as a label on the fill itself),
     so the aqua zone below overrides this to its dark-fill / white-label
     inversion instead of trying to make cyan carry a white label (2.20:1). */
  --cta-bg: var(--brand-cyan);
  --cta-fg: var(--brand-navy);
  --cta-bg-hover: var(--brand-cyan-lift);
  --cta-ring: transparent;
  --cta-rule: transparent;

  --state-error-dark: #f4816c;         /* 6.58:1 on ink */
  --state-error-paper: #8a2b1c;        /* dark red, comfortably >4.5:1 on aqua */
  --state-ok-dark: #4ecb92;            /* 8.25:1 on ink */
  --state-ok-paper: #1f6b45;           /* dark green, comfortably >4.5:1 on aqua */
  --state-error: var(--state-error-dark);
  --state-ok: var(--state-ok-dark);

  /* --- Typography ----------------------------------------------------------
     Quantify is brand/logo only -- the wordmark (.brand__word), nothing
     else. Switzer (variable, 100-900) carries every display/title role as
     well as body, lead, labels, nav and UI text, so a real weight axis is
     available across the whole hierarchy. No monospaced face ships. ------- */
  --font-display: "Switzer", "Segoe UI Variable Text", system-ui, sans-serif;
  --font-text: "Switzer", "Segoe UI Variable Text", system-ui, sans-serif;
  --font-brand: "Quantify", "Switzer", "Segoe UI Variable Display", system-ui, sans-serif;

  --fs-display-hero: clamp(2.625rem, 1.35rem + 5.44vw, 5.75rem);
  --fs-display-section: clamp(1.875rem, 1.3rem + 2.4vw, 3.25rem);
  /* B1 hierarchy rebuild: with every --font-display rule now the font's one
     real weight (400), the sub-display step is bumped ~2px at each end of
     the clamp to help carry the separation that the fake weight steps used
     to fake. Size/tracking/colour/space now do this job, never weight. */
  --fs-display-sub: clamp(1.625rem, 1.36rem + 1.07vw, 2.375rem);
  --fs-title-module: clamp(1.125rem, 1.06rem + 0.28vw, 1.3125rem);
  --fs-lead: clamp(1.0625rem, 1rem + 0.27vw, 1.3125rem);
  --fs-body: clamp(1rem, 0.977rem + 0.1vw, 1.0625rem);
  --fs-body-sm: 0.9375rem;
  --fs-caption: 0.8125rem;
  --fs-label-tech: 0.6875rem;
  --fs-label-tech-lg: 0.8125rem;

  --lh-hero: 0.96;
  --lh-section: 1.02;
  --lh-sub: 1.1;
  --lh-module: 1.3;
  --lh-lead: 1.55;
  --lh-body: 1.65;

  /* Switzer now carries display tracking (Quantify no longer does -- it is
     restricted to the wordmark). Switzer is a grotesque, not a geometric
     face, so it wants the mild negative tracking a grotesque display size
     normally takes, tuned back from the original spec's -0.03/-0.025/-0.02em
     table to avoid the collisions the earlier Quantify-era QA pass found. */
  --track-hero: -0.02em;
  --track-section: -0.018em;
  --track-sub: -0.012em;
  /* +0.14em is the one surviving uppercase-tracked style: the section
     marker. Nothing else on the site takes positive tracking. */
  --track-label: 0.14em;
  --track-label-lg: 0.14em;

  /* --- Spacing, 4px base ------------------------------------------------- */
  --space-3xs: 0.25rem;
  --space-2xs: 0.5rem;
  --space-xs: 0.75rem;
  --space-sm: 1rem;
  --space-md: 1.5rem;
  --space-lg: 2rem;
  --space-xl: 3rem;
  --space-2xl: 4rem;
  --space-3xl: 6rem;
  --space-4xl: 8rem;

  --section-pad-y: clamp(4rem, 2.2rem + 7.7vw, 8.5rem);
  --section-pad-y-tight: clamp(2.5rem, 1.6rem + 3.8vw, 4.5rem);
  --stack-gap: clamp(1.5rem, 1.1rem + 1.7vw, 2.5rem);

  /* --- Shape. Shape Consistency Lock: 0 / 2 / 4, no pills. --------------- */
  --radius-flat: 0px;      /* hairline rules, spec-grid cells, tables -- stays square */
  --radius-unit: 4px;      /* small controls: inputs, tabs, chips, spec chips */
  --radius-control: 10px;  /* buttons, stepper controls, close buttons */
  --radius-vessel: 16px;   /* cards, panels, dialogs */
  --radius-plate: 28px;    /* feature imagery, large media plates */
  --radius-pill: 999px;    /* segmented filter pills + numeric counters ONLY */

  /* --- Containers and grid ----------------------------------------------- */
  --container-max: 1320px;
  --container-wide: 1560px;
  --container-narrow: 760px;
  --measure: 68ch;
  --measure-tight: 54ch;

  --gutter: clamp(1.25rem, 0.85rem + 1.7vw, 2.5rem);
  --grid-gap: clamp(1rem, 0.7rem + 1.3vw, 1.75rem);
  --grid-gap-y: clamp(1.5rem, 1rem + 2.1vw, 3rem);

  /* Navbar geometry (creative-direction-v2 Sec 5.1). Nothing here shrinks or
     re-flows on scroll -- only these two states, cross-faded. */
  --nav-h-lg: 80px;         --nav-h-md: 72px;         --nav-h-sm: 64px;
  --nav-h-lg-condensed: 72px; --nav-h-md-condensed: 64px; --nav-h-sm-condensed: 58px;
  --nav-h: var(--nav-h-md);
  --nav-h-mobile: var(--nav-h-sm);
  --join-overlap: clamp(40px, 6vw, 96px);

  /* --- Elevation. Tinted, never neutral black. Genuine drop shadows on the
     inquiry panel only -- never used to fake a soft directional wash. ---- */
  --shadow-paper-raise: 0 1px 2px rgb(6 32 69 / 0.05), 0 10px 28px -14px rgb(6 32 69 / 0.16);
  --shadow-paper-lift: 0 2px 4px rgb(6 32 69 / 0.06), 0 22px 52px -20px rgb(6 32 69 / 0.22);
  --shadow-dark-raise: 0 1px 2px rgb(2 6 16 / 0.4), 0 14px 34px -18px rgb(2 6 16 / 0.65);
  --shadow-dark-lift: 0 2px 6px rgb(2 6 16 / 0.45), 0 30px 70px -26px rgb(2 6 16 / 0.75);
  --shadow-overlay: 0 48px 140px -40px rgb(2 6 16 / 0.72);

  --rim-dark: inset 0 1px 0 rgb(255 255 255 / 0.06);
  --rim-paper: inset 0 1px 0 rgb(255 255 255 / 0.92);

  /* Focus ring (D11 rule 6): navy on aqua, cyan on dark. Cyan is never used
     as a ring on aqua -- 1.46:1 there, below the 3:1 non-text floor. */
  --ring: 0 0 0 2px var(--field-ink), 0 0 0 4px var(--brand-cyan);
  --ring-paper: 0 0 0 2px var(--field-paper), 0 0 0 4px var(--brand-navy);

  /* --- Easing (creative-direction-v2 Sec 7.1). No linear, no ease, no
     ease-in-out, no spring overshoot anywhere. -------------------------- */
  --ease-arrive: cubic-bezier(0.16, 1, 0.30, 1);      /* expo-out, default entrance */
  --ease-travel: cubic-bezier(0.65, 0, 0.35, 1);      /* symmetric, things crossing the frame */
  --ease-detent: cubic-bezier(0.86, 0, 0.07, 1);      /* mechanical latch */
  --ease-mass:   cubic-bezier(0.32, 0.72, 0, 1);      /* heavy surfaces: panels, shutters */
  --ease-depart: cubic-bezier(0.55, 0, 1, 0.45);      /* expo-in, exits only */
  --ease-draw:   cubic-bezier(0.40, 0.05, 0.60, 0.95);/* near-constant rate, line drawing */

  /* Legacy aliases kept so existing components need not be touched one by
     one; each now points at its Sec 7.1 equivalent. */
  --ease-signal: var(--ease-arrive);
  --ease-instrument: var(--ease-mass);
  --ease-exit: var(--ease-depart);
  --ease-trace: var(--ease-draw);
  --ease-settle: var(--ease-arrive);

  /* --- Durations (creative-direction-v2 Sec 7.1) -------------------------- */
  --dur-1: 120ms;  --dur-2: 200ms;  --dur-3: 300ms;  --dur-4: 440ms;
  --dur-5: 640ms;  --dur-6: 900ms;  --dur-7: 1200ms;
  --stagger-1: 60ms; --stagger-2: 90ms;

  --dur-instant: var(--dur-1);
  --dur-tick: var(--dur-2);
  --dur-fast: var(--dur-2);
  --dur-base: var(--dur-4);
  --dur-settle: var(--dur-5);
  --dur-reveal: var(--dur-5);
  --dur-heavy: var(--dur-6);
  --stagger: var(--stagger-1);

  /* --- Layers. Systemic only. -------------------------------------------- */
  --z-base: 0;
  --z-raised: 10;
  --z-sticky: 100;
  --z-nav: 200;
  --z-scrim: 300;
  --z-overlay: 400;
  --z-intro: 900;
  --z-skip: 950;

  --hairline: 1px;
}

/* --------------------------------------------------------------------------
   3. Section field system
   Every <section> (and the footer) carries data-field="dark" or
   data-field="aqua" directly -- no wrapper div. Every component inside reads
   the unsuffixed aliases and adapts without a single component-level
   override. This same attribute is what the navbar's IntersectionObserver
   reads to drive adaptive contrast (Sec 5.4). There is no third "navy"
   field: Law 2 (navy measures 1.04:1 against dark -- effectively invisible
   there) means navy is never a section background, only the ink of the
   aqua field, so every v1 ".field--navy" section is re-cast to dark or aqua.
   Seams between fields are the three named joints (butt / datum / lap), not
   a coloured gasket -- cyan at a seam would be invisible on half the site's
   boundaries (1.46:1 on aqua), so no universal seam rule exists any more.
   -------------------------------------------------------------------------- */

[data-field] {
  position: relative;
}

[data-field="dark"] {
  background: var(--field-ink);
  --surface-base: var(--brand-ink);
  --surface-raised: var(--brand-ink-lift);
  --surface-sunken: rgb(255 255 255 / 0.05);
  --text-primary: var(--text-ink-primary);
  --text-secondary: var(--text-ink-secondary);
  --text-muted: var(--text-ink-muted);
  --text-faint: var(--text-ink-faint);
  --text-disabled: var(--text-ink-disabled);
  --text-signal: var(--text-ink-signal);
  --text-accent: var(--text-ink-aqua);
  --line-hair: var(--line-ink-hair);
  --line-soft: var(--line-ink-soft);
  --line-strong: var(--line-ink-strong);
  --state-error: var(--state-error-dark);
  --state-ok: var(--state-ok-dark);
  --ring: 0 0 0 2px var(--field-ink), 0 0 0 4px var(--brand-cyan);
  --cta-bg: var(--brand-cyan);
  --cta-fg: var(--brand-navy);
  --cta-bg-hover: var(--brand-cyan-lift);
  --cta-ring: transparent;
  --cta-rule: transparent;
  color: var(--text-primary);
}

[data-field="aqua"] {
  background: var(--field-paper);
  --surface-base: var(--surface-paper-base);
  /* B3/M2 cleanup: --surface-paper-raised (#b2e6f0) was deleted -- its only
     consumer, the capability card, is now a flat #ffffff (M2), and no
     other aqua component reads --surface-raised, so this points at the
     same white rather than keeping an otherwise-unused bespoke hex alive. */
  --surface-raised: #ffffff;
  --surface-sunken: var(--surface-paper-sunken);
  --text-primary: var(--text-paper-primary);
  --text-secondary: var(--text-paper-secondary);
  --text-muted: var(--text-paper-muted);
  --text-faint: var(--text-paper-faint);
  --text-disabled: var(--text-paper-disabled);
  --text-signal: var(--text-paper-signal);
  --text-accent: var(--text-paper-signal);
  --line-hair: var(--line-paper-hair);
  --line-soft: var(--line-paper-soft);
  --line-strong: var(--line-paper-strong);
  --signal: var(--brand-cyan-deep);
  --signal-hover: #0a4a50;
  --signal-wash: var(--signal-wash-paper);
  --signal-ink: var(--brand-white);
  --state-error: var(--state-error-paper);
  --state-ok: var(--state-ok-paper);
  --ring: var(--ring-paper);
  --shadow-raise: var(--shadow-paper-raise);
  /* Primary control inverts on aqua: dark fill, white label (16.85:1) --
     cyan on white/aqua fails (2.20:1 / 1.46:1), so the fill can never be
     cyan here. The cyan rule survives as a 2px inset accent on the fill
     itself (7.66:1 against #1c1c28), not as the fill colour. */
  --cta-bg: var(--brand-ink);
  --cta-fg: var(--brand-white);
  --cta-bg-hover: var(--brand-ink);
  --cta-ring: var(--brand-cyan);
  --cta-rule: var(--brand-cyan);
  color: var(--text-primary);
}

/* The inquiry modal only. White ground, not a section field. */
[data-field="panel"] {
  background: #ffffff;
  --surface-base: #ffffff;
  --surface-raised: #ffffff;
  --surface-sunken: #eef7fa;
  --text-primary: #062045;
  --text-secondary: #183759;
  --text-muted: #2f5573;
  --text-faint: #39627f;
  --text-signal: var(--brand-cyan-deep);
  --text-accent: var(--brand-cyan-deep);
  --line-hair: rgb(6 32 69 / 0.12);
  --line-soft: rgb(6 32 69 / 0.20);
  --line-strong: rgb(6 32 69 / 0.36);
  --signal: var(--brand-cyan-deep);
  --signal-ink: #ffffff;
  --ring: 0 0 0 2px #ffffff, 0 0 0 4px var(--brand-navy);
  --cta-bg: var(--brand-ink);
  --cta-fg: var(--brand-white);
  --cta-ring: var(--brand-cyan);
  --cta-rule: var(--brand-cyan);
  color: var(--text-primary);
}

/* --------------------------------------------------------------------------
   4. Reset and base
   -------------------------------------------------------------------------- */

*,
*::before,
*::after {
  box-sizing: border-box;
}

* {
  margin: 0;
}

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  scroll-padding-top: calc(var(--nav-h) + 1.5rem);
  /* Quantify (wordmark only) is a static, single-weight face (usWeightClass
     400, no fvar/STAT master) and would still be silently faked by the
     browser if a weight were ever requested on it. Declared here, inherited
     by the whole document, so that constraint can never regress on the
     wordmark. Switzer's real variable axis makes the rest of the weight
     range below (h1/h2/h3, .hero__title) genuine masters, not synthesis. */
  font-synthesis: none;
}

body {
  min-height: 100svh;
  background-color: var(--field-ink);
  color: var(--text-ink-primary);
  font-family: var(--font-text);
  font-size: var(--fs-body);
  font-weight: 400;
  line-height: var(--lh-body);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

html[data-scroll-locked] body {
  overflow: hidden;
}

img,
picture,
svg,
video,
canvas {
  display: block;
  max-width: 100%;
}

img,
video {
  height: auto;
}

input,
button,
textarea,
select {
  font: inherit;
  color: inherit;
}

button {
  background: none;
  border: 0;
  cursor: pointer;
}

ul,
ol {
  padding: 0;
  list-style: none;
}

a {
  color: inherit;
  text-underline-offset: 0.22em;
}

/* Task 2 fix: inline <use> icons (arrow, close, carousel/stepper chevrons)
   have no fill of their own and were defaulting to the UA black. Every icon
   now inherits the colour of its adjacent label/button text. Element-level
   specificity (0,0,1) is intentionally low so the explicit .mark__path /
   .intro__path fills (class selectors, higher specificity) keep winning
   regardless of source order. */
svg {
  fill: currentColor;
}

:focus-visible {
  outline: none;
  box-shadow: var(--ring);
  border-radius: var(--radius-unit);
}

::selection {
  background: var(--brand-cyan);
  color: var(--brand-navy);
}

.skip-link {
  position: fixed;
  top: var(--space-xs);
  left: var(--space-xs);
  z-index: var(--z-skip);
  padding: 0.75rem 1.25rem;
  background: var(--brand-cyan);
  color: var(--brand-navy);
  font-family: var(--font-text);
  font-size: var(--fs-body-sm);
  font-weight: 600;
  text-decoration: none;
  border-radius: var(--radius-unit);
  transform: translateY(calc(-100% - var(--space-lg)));
  transition: transform var(--dur-base) var(--ease-signal);
}

.skip-link:focus-visible {
  transform: translateY(0);
}

.visually-hidden {
  position: absolute !important;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* --------------------------------------------------------------------------
   5. Layout primitives
   -------------------------------------------------------------------------- */

.shell {
  width: 100%;
  max-width: var(--container-max);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.shell--wide {
  max-width: var(--container-wide);
}

.shell--narrow {
  max-width: var(--container-narrow);
}

.section {
  padding-block: var(--section-pad-y);
}

.section--tight {
  padding-block: var(--section-pad-y-tight);
}

/* BLOCKER B3 fix: this used to fill with --surface-sunken, a flat hex a
   ~10% value shift from the section's own ground -- as a 700-850px
   full-bleed band it read as a rendering fault, not a second brand tone
   (worst on index, sandwiched between two identical #98deeb fields). The
   site now has exactly two grounds (#1c1c28, #98deeb); this class marks the
   seam with a hairline instead of a colour step. */
.section--sunken {
  border-block-start: var(--hairline) solid var(--line-soft);
}

.stack {
  display: grid;
  gap: var(--flow, var(--space-sm));
  align-content: start;
}

.stack--md {
  --flow: var(--space-md);
}

.stack--lg {
  --flow: var(--stack-gap);
}

/* --------------------------------------------------------------------------
   6. Typography
   -------------------------------------------------------------------------- */

h1,
h2,
h3 {
  font-family: var(--font-display);
  /* Switzer (variable, 100-900) carries this role now, not Quantify -- a
     real master exists at 500, so the previous forced 400 (needed only to
     avoid synthetic bolding on Quantify's single static weight) no longer
     applies here. font-synthesis: none (Sec 1/Sec 4) stays in force but is
     harmless: 500 is a genuine weight, not a faked one. */
  font-weight: 500;
  letter-spacing: var(--track-section);
  line-height: var(--lh-section);
  text-wrap: balance;
}

h1 {
  font-size: var(--fs-display-section);
  line-height: var(--lh-section);
}

h2 {
  font-size: var(--fs-display-section);
}

h3 {
  font-size: var(--fs-display-sub);
  font-weight: 500;
  letter-spacing: var(--track-sub);
  line-height: var(--lh-sub);
}

h4,
h5 {
  font-family: var(--font-text);
  font-size: var(--fs-title-module);
  font-weight: 600;
  line-height: var(--lh-module);
  /* Tracking law (creative-direction-v2 Sec 2.4): 15-31px takes zero
     tracking. fs-title-module tops out at 21px, so -0.008em was a law
     violation; corrected alongside the HIGH 3 audit. */
  letter-spacing: 0;
}

p {
  max-width: var(--measure);
  text-wrap: pretty;
}

.hero-title {
  font-size: var(--fs-display-hero);
  line-height: var(--lh-hero);
  letter-spacing: var(--track-hero);
  font-weight: 400;
}

.lead {
  max-width: var(--measure-tight);
  font-size: var(--fs-lead);
  line-height: var(--lh-lead);
  /* Tracking law: 15-31px is zero (fs-lead tops out at 21px). */
  letter-spacing: 0;
  color: var(--text-secondary);
}

.body-sm {
  font-size: var(--fs-body-sm);
  line-height: 1.6;
}

.caption {
  font-size: var(--fs-caption);
  line-height: 1.45;
  color: var(--text-muted);
}

.muted {
  color: var(--text-secondary);
}

.faint {
  color: var(--text-muted);
}

/* Uppercase is permitted in exactly four places on the site (creative-
   direction-v2 Sec 2.4): the wordmark, the section marker below, acronyms
   inside running copy (styled with no special treatment at all), and
   --t-display/--t-section under a caps-only brandface. Everything else,
   including nav, buttons, form labels and footer headings, is sentence
   case. .label-tech (mono, uppercase, the single most-used v1 component)
   is deleted; .field-label, .spec-term and .section-marker replace it. */

.field-label {
  display: inline-block;
  font-family: var(--font-text);
  font-size: var(--fs-body-sm);
  font-weight: 500;
  color: var(--text-secondary);
}

.spec-term {
  font-family: var(--font-text);
  font-size: var(--fs-body-sm);
  font-weight: 500;
  color: var(--text-primary);
}

/* The section marker: the one surviving uppercase-tracked style. Rationed
   to one per section, ceil(sections / 3) per page. */
.section-marker,
.eyebrow {
  display: block;
  font-family: var(--font-text);
  font-size: clamp(0.75rem, 0.728rem + 0.094vw, 0.8125rem);
  font-weight: 600;
  line-height: 1.3;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--text-signal);
}

/* HIGH H10 fix. #1ec1cb is illegal as text on aqua (D11, the Cyan Law:
   1.46:1), so --text-signal there resolves to --brand-cyan-deep (#0e5c63)
   as an accent substitute -- correct as a general-purpose accent hue, but
   on the eyebrow specifically it read as a washed, off-palette teal
   ("CAPABILITIES", "ABOUT SIMSAN"). Don't substitute here: eyebrows on
   aqua use the same navy ink as the heading below them and differentiate
   purely by size, caps and letterspacing. On dark the eyebrow keeps the
   cyan it already had right (--text-signal there is the real brand cyan,
   legal and correct), so this is scoped to the aqua field only. */
[data-field="aqua"] .section-marker,
[data-field="aqua"] .eyebrow {
  color: var(--text-primary);
}

.signal-word {
  color: var(--text-signal);
}

/* --------------------------------------------------------------------------
   7. Buttons and links
   -------------------------------------------------------------------------- */

.btn {
  --btn-bg: transparent;
  --btn-fg: var(--text-primary);
  --btn-line: var(--line-strong);
  display: inline-flex;
  align-items: center;
  gap: 0.625rem;
  min-height: 3rem;
  padding: 0.875rem 1.5rem;
  background: var(--btn-bg);
  color: var(--btn-fg);
  border: var(--hairline) solid var(--btn-line);
  border-radius: var(--radius-control);
  font-family: var(--font-text);
  font-size: var(--fs-body-sm);
  font-weight: 600;
  text-decoration: none;
  white-space: nowrap;
  transition:
    background-color var(--dur-2) var(--ease-arrive),
    border-color var(--dur-2) var(--ease-arrive),
    color var(--dur-2) var(--ease-arrive);
}

/* M8 Mechanical Timing. No magnetic pointer-follow, no spring overshoot --
   the button itself presses; nothing inside it shifts independently. */
.btn:active {
  transform: scale(0.985);
  transition-duration: var(--dur-1);
}

/* A bare arrow glyph. No icon box, no tinted square, no second radius. */
.btn__arrow {
  display: inline-flex;
  align-items: center;
  width: 0.875rem;
  height: 0.875rem;
  flex: none;
}

.btn__arrow svg {
  width: 100%;
  height: 100%;
}

/* The one solid-cyan / dark-fill primary control. Works on both grounds via
   the --cta-* tokens each [data-field] block sets -- one flat fill, no
   per-section variant class. */
.btn--cta {
  --btn-bg: var(--cta-bg, var(--signal));
  --btn-fg: var(--cta-fg, var(--signal-ink));
  --btn-line: var(--cta-bg, var(--signal));
  box-shadow: inset 0 -2px 0 0 var(--cta-rule, transparent);
}

.btn--cta:hover {
  --btn-bg: var(--cta-bg-hover, var(--signal-hover));
}

.btn--cta:focus-visible,
.btn--cta:hover {
  box-shadow: inset 0 0 0 2px var(--cta-ring, transparent);
}

.btn--ghost:hover {
  --btn-line: var(--signal);
  --btn-fg: var(--text-signal);
}

.btn[disabled] {
  opacity: 0.55;
  cursor: not-allowed;
}

.btn-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
}

/* Quiet directional link. M2 Traced Line: the rule extends on hover. */
.link {
  display: inline-flex;
  align-items: center;
  min-height: 2.75rem;
  gap: var(--space-2xs);
  font-family: var(--font-text);
  font-size: var(--fs-body-sm);
  font-weight: 500;
  color: var(--text-signal);
  text-decoration: none;
}

.link::after {
  content: "";
  width: 1.5rem;
  height: var(--hairline);
  background: currentColor;
  transition: width var(--dur-2) var(--ease-draw);
}

.link:hover::after,
.link:focus-visible::after {
  width: 2.5rem;
}

/* --------------------------------------------------------------------------
   8. Motion system
   The nine primitives of creative-direction-v2 Sec 7.3. IntersectionObserver
   entries unobserve after first fire; nothing re-animates on scroll-back.
   Every primitive has a reduced-motion fallback in Section 18.
   -------------------------------------------------------------------------- */

/* M1 Precision Reveal. The single entrance animation on the site. */
[data-reveal] {
  --i: 0;
}

[data-reveal][data-reveal-state="out"] {
  opacity: 0;
  transform: translate3d(0, 20px, 0);
}

[data-reveal][data-reveal-state="in"] {
  opacity: 1;
  transform: none;
  transition:
    opacity var(--dur-5) var(--ease-arrive) calc(var(--i) * var(--stagger-1)),
    transform var(--dur-5) var(--ease-arrive) calc(var(--i) * var(--stagger-1));
}

/* M2 Traced Line: a 1px rule draws from one end to the other and holds. */
.trace-rule,
.datum-rule {
  position: relative;
  height: var(--hairline);
  background: var(--line-hair);
}

.trace-rule::after,
.datum-rule::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--signal);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--dur-6) var(--ease-draw) var(--trace-delay, 0ms);
}

[data-reveal-state="in"] .trace-rule::after,
[data-reveal-state="in"] .datum-rule::after,
.trace-rule[data-reveal-state="in"]::after {
  transform: scaleX(1);
}

/* Vertical variant used beside the positioning statement. */
.trace-rule--v {
  width: var(--hairline);
  height: auto;
  align-self: stretch;
  background: var(--line-hair);
}

.trace-rule--v::after {
  transform: scaleY(0);
  transform-origin: top;
  transition: transform var(--dur-6) var(--ease-draw);
}

[data-reveal-state="in"] .trace-rule--v::after {
  transform: scaleY(1);
}

/* Self-fallback counterpart to the ancestor-based rule above, mirroring
   .trace-rule[data-reveal-state="in"]::after (Sec 8) -- lets a
   .trace-rule--v carry its own [data-reveal] instead of always depending on
   an observed ancestor. Additive only; no existing markup gives
   .trace-rule--v its own data-reveal today, so this is inert everywhere
   except new usage (about.html Sec 22). */
.trace-rule--v[data-reveal-state="in"]::after {
  transform: scaleY(1);
}

/* M3 Node Activation. A 7px square, never a circle or a dot. One 1px ring
   expands 7px -> 13px and fades. No glow, no blur, no drop-shadow. */
.node-marker {
  position: relative;
  width: 7px;
  height: 7px;
  flex: none;
  background: transparent;
  border: 1px solid var(--line-strong);
  transition:
    background-color var(--dur-3) var(--ease-detent),
    border-color var(--dur-3) var(--ease-detent);
}

.node-marker::after {
  content: "";
  position: absolute;
  inset: 0;
  border: 1px solid var(--signal);
  opacity: 0;
}

.node-marker[data-state="active"],
[data-reveal-state="in"] .node-marker {
  background: var(--signal);
  border-color: var(--signal);
}

.node-marker[data-state="active"]::after,
[data-reveal-state="in"] .node-marker::after {
  animation: node-ring var(--dur-4) var(--ease-detent) both;
}

/* M6 Scroll-Linked Image Movement. An image inside overflow: hidden is 116%
   of its frame's height and translates Y across the frame's viewport
   transit, written by JS as a plain translate3d -- no duration, no easing,
   position-linked only. Cap: 3 per page. Also the lap-joint plate: it
   straddles a field seam, ~62% into the incoming field and ~38% into the
   outgoing one, via a negative margin-block-start equal to --join-overlap. */
.lap-plate {
  position: relative;
  z-index: 1;
  margin-block-start: calc(var(--join-overlap) * -1);
  overflow: hidden;
  aspect-ratio: 21 / 9;
}

.lap-plate--16-9 {
  aspect-ratio: 16 / 9;
}

.lap-plate img {
  width: 100%;
  height: 116%;
  object-fit: cover;
  transform: translate3d(0, 0, 0);
}

/* Portrait/square companion figures elsewhere on the site reuse the same
   M6 "116% height, translate -8%..+8%" treatment via this shared rule. */
.capability__figure-img,
.about-hero__figure-img,
.about-detail__figure-img {
  width: 100%;
  height: 116%;
  object-fit: cover;
  transform: translate3d(0, 0, 0);
}

@keyframes node-ring {
  from {
    inset: 0;
    opacity: 0.8;
  }
  to {
    inset: -3px;
    opacity: 0;
  }
}

/* M5 Controlled Mask. A straight-edged clip-path wipe. Never feathered,
   never diagonal, never a soft-edged mask. */
[data-mask] {
  clip-path: inset(0 0 100% 0);
  transition: clip-path var(--dur-5) var(--ease-mass);
}

[data-mask][data-reveal-state="in"] {
  clip-path: inset(0 0 0% 0);
}

/* --------------------------------------------------------------------------
   9. Logo intro
   One compound path, one trace: stroke-dasharray/stroke-dashoffset animate
   from the mark's measured length to 0, then a hard cut to a solid
   currentColor fill. No glow, no per-lobe timing, no outer frame.
   -------------------------------------------------------------------------- */

.intro {
  position: fixed;
  inset: 0;
  z-index: var(--z-intro);
  display: grid;
  place-items: center;
  background: var(--brand-ink);
  transition: opacity var(--dur-3) var(--ease-depart);
}

html[data-intro-active] body {
  overflow: hidden;
}

/* Without JavaScript the overlay must never cover the page. */
html:not([data-js]) .intro {
  display: none;
}

.intro[data-state="out"] {
  opacity: 0;
  pointer-events: none;
}

.intro__stage {
  width: clamp(5rem, 14vw, 7.5rem);
  transform-origin: center center;
}

.intro__mark {
  width: 100%;
  height: auto;
  overflow: visible;
}

.intro__path {
  fill: var(--brand-cyan);
  fill-opacity: 0;
  stroke: var(--brand-cyan);
  stroke-width: 1.4;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.intro[data-state="bloom"] .intro__path,
.intro[data-state="hold"] .intro__path,
.intro[data-state="static"] .intro__path,
.intro[data-state="out"] .intro__path {
  fill-opacity: 1;
  stroke-opacity: 0;
  transition: fill-opacity var(--dur-3) var(--ease-arrive), stroke-opacity var(--dur-3) var(--ease-arrive);
}

/* The hero holds still until the intro hands over. */
html[data-intro-active] .hero [data-reveal] {
  transition: none;
}

/* --------------------------------------------------------------------------
   10. Header and navigation
   -------------------------------------------------------------------------- */

.header-sentinel {
  position: absolute;
  top: 0;
  width: 1px;
  height: 1px;
  pointer-events: none;
}

/* Adaptive contrast (Sec 5.4). One IntersectionObserver over every
   [data-field] element writes data-nav-over on <html>; the three treatments
   below (dark / aqua / media) are pure CSS reacting to that one attribute.
   No scroll listener, no pixel sampling, no canvas. */
html {
  --nav-label: rgb(255 255 255 / 0.78);
  --nav-label-strong: #ffffff;
  --nav-signal: var(--brand-cyan);
  --nav-mark-color: var(--brand-cyan);
  /* At rest the bar is fully transparent on every ground, including the
     hero video (BLOCKER 1). The translucent plate is the condensed
     (data-stuck="true") state only, below -- never the resting state. */
  --nav-plate: transparent;
  --nav-blur: 0px;
  --nav-text-shadow: none;
  --nav-icon-shadow: none;
}

/* Real bug found in QA (Wave 6), superseded by MEDIUM M2: this used to be
   scoped to [data-stuck="true"] only, because at rest .page-hero started
   below the header and the page's own dark-ink body background peeked
   through instead -- so white nav text at rest was correct there. M2 gave
   .page-hero[data-field="aqua"] (about.html) the same negative-margin
   treatment .hero uses, so the aqua field now genuinely sits behind the
   bar from the very first frame, not just once condensed. Dark-navy nav
   text is therefore now correct for the WHOLE "aqua" resting state, not
   only the stuck/condensed one -- the stuck-only restriction is dropped.
   This never fires elsewhere: no aqua [data-field] section sits at the top
   of the intersection band on any other page. */
html[data-nav-over="aqua"] .site-header {
  --nav-label: #183759;
  --nav-label-strong: #062045;
  --nav-signal: #062045;
  --nav-mark-color: #062045;
}

/* Over the hero at rest there is no plate at all. The previous pass tested
   only white against the daylight opening frame and, finding 2.71:1
   (below the 3:1 non-text floor), propped it up with a navy text-shadow --
   which WCAG 1.4.3 gives no credit for, because that criterion is defined
   on flat foreground/background colour. Re-measured properly: the top 96px
   of the actual rendered video, sampled live across the whole scrub
   (p = 0 / .15 / .30 / .45 / .60 / .80 / 1.0), averages rgb(125,157,226),
   (124,158,228), (107,144,219), (10,12,42), (11,13,43), (11,13,43),
   (11,13,43) -- a bright sky fading to near-black night. Against that:

     p     white    dark(#1c1c28)
     0     2.69:1   6.25:1
     0.15  2.67:1   6.31:1
     0.30  3.16:1   5.33:1
     0.45  19.11:1  1.13:1
     0.60  18.98:1  1.13:1
     0.80  18.98:1  1.13:1
     1.0   18.98:1  1.13:1

   Dark ink wins comfortably (AA, 4.5:1+) through p<=0.30 and *improves* as
   the frame does, then collapses once the grade turns to night (a finer
   sweep p=0.31..0.43 shows neither foreground clears 4.5:1 in the narrow
   p~0.32-0.34 band -- a real dead zone for flat colour against this
   specific clip). That dead zone is never shown to a user, though: the
   sentinel that flips data-stuck sits at the literal top of the document,
   so data-stuck goes true within ~2px of scroll (p ~ 0.0005) -- while the
   frame is still at its brightest -- long before p reaches the collapse
   band. So the dark treatment below is scoped to the unstuck instant only.
   The moment the header is stuck, the existing dark translucent plate
   (this section, below) takes over regardless of scene, and the ambient
   white treatment (the html default above) applies again, unchanged. No
   text-shadow: the flat ratio already clears AA without one. */
html[data-nav-over="media"] .site-header:not([data-stuck="true"]) {
  --nav-label: var(--brand-ink);
  --nav-label-strong: var(--brand-ink);
  --nav-signal: var(--brand-ink);
  --nav-mark-color: var(--brand-ink);
  --nav-text-shadow: none;
  --nav-icon-shadow: none;
}

.site-header {
  position: sticky;
  top: 0;
  z-index: var(--z-sticky);
  border-block-end: var(--hairline) solid transparent;
  background: var(--nav-plate);
  backdrop-filter: blur(var(--nav-blur)) saturate(115%);
  -webkit-backdrop-filter: blur(var(--nav-blur)) saturate(115%);
  will-change: transform;
  transition:
    background-color var(--dur-travel, 260ms) var(--ease-travel),
    border-color var(--dur-travel, 260ms) var(--ease-travel),
    backdrop-filter var(--dur-travel, 260ms) var(--ease-travel),
    transform var(--dur-3) var(--ease-arrive);
}

/* Scroll-direction hide/show contract: JS toggles [data-hidden] on the
   existing [data-header] element. Transform only -- never top/height, so
   this never fights .site-header's own sticky/height transitions above. */
.site-header[data-hidden="true"] {
  transform: translate3d(0, -100%, 0);
}

/* Defensive: the JS guarantees it never sets data-hidden="true" while the
   mobile drawer is open, but a stuck attribute must not be able to hide the
   drawer's own header. The drawer's open state sets this scroll lock. */
html[data-scroll-locked] .site-header[data-hidden="true"] {
  transform: none;
}

/* Condensed: driven by the existing header-sentinel IO. Height steps down
   one rung; nothing inside re-flows, scales or disappears. */
.site-header[data-stuck="true"] {
  --nav-blur: 16px;
  border-block-end-color: var(--line-ink-soft);
}

html:not([data-nav-over="aqua"]) .site-header[data-stuck="true"] {
  background: rgb(28 28 40 / 0.72);
}

html[data-nav-over="aqua"] .site-header[data-stuck="true"] {
  background: rgb(152 222 235 / 0.82);
  border-block-end-color: var(--line-paper-soft);
}

.site-header__inner {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  min-height: var(--nav-h-sm);
  max-width: var(--container-wide);
  margin-inline: auto;
  padding-inline: var(--gutter);
  transition: min-height var(--dur-4) var(--ease-mass);
}

.site-header[data-stuck="true"] .site-header__inner {
  min-height: var(--nav-h-sm-condensed);
}

.brand {
  display: inline-flex;
  align-items: center;
  min-height: 2.75rem;
  gap: 0.75rem;
  color: var(--nav-label-strong);
  text-decoration: none;
}

.brand__mark {
  width: 1.625rem;
  height: 1.625rem;
  flex: none;
  color: var(--nav-mark-color);
  transition: color var(--dur-travel, 260ms) var(--ease-travel);
}

/* The dark room: the mobile drawer and footer lockups are never adaptive. */
.brand--on-dark .brand__mark,
.mobile-nav .brand__mark,
.site-footer .brand__mark {
  color: var(--brand-cyan);
}

.brand--on-dark,
.mobile-nav .brand,
.site-footer .brand {
  color: #ffffff;
}

.brand__word {
  /* Quantify's only allowed use on the site: the wordmark. */
  font-family: var(--font-brand);
  font-size: 1.0625rem;
  font-weight: 400;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  line-height: 1;
}

.brand__suffix {
  display: none;
  padding-inline-start: 0.625rem;
  margin-inline-start: 0.125rem;
  border-inline-start: var(--hairline) solid currentColor;
  opacity: 0.5;
  font-family: var(--font-text);
  font-size: 0.75rem;
  font-weight: 500;
  color: inherit;
}

/* The nav-context text-shadow (BLOCKER 1) is scoped to the header only --
   the same .brand__word/.brand__suffix classes are reused in the mobile
   drawer and footer, which are never "over media" and must never inherit
   this shadow. */
.site-header .brand__word,
.site-header .brand__suffix {
  text-shadow: var(--nav-text-shadow, none);
}

.site-header .brand__mark {
  filter: var(--nav-icon-shadow, none);
}

.nav {
  display: none;
  margin-inline-start: auto;
}

.nav__list {
  display: flex;
  align-items: center;
  gap: clamp(1.5rem, 2.2vw, 2.25rem);
}

.nav__link {
  position: relative;
  display: inline-flex;
  align-items: center;
  min-height: 2.75rem;
  font-family: var(--font-text);
  font-size: var(--fs-body-sm);
  font-weight: 500;
  text-decoration: none;
  color: var(--nav-label);
  text-shadow: var(--nav-text-shadow, none);
  transition: color var(--dur-travel, 260ms) var(--ease-travel), text-shadow var(--dur-travel, 260ms) var(--ease-travel);
}

.nav__link::after {
  content: "";
  position: absolute;
  inset-inline: 0;
  bottom: 0;
  height: var(--hairline);
  background: var(--nav-signal);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--dur-2) var(--ease-draw);
}

.nav__link:hover,
.nav__link:focus-visible,
.nav__link[aria-current="page"] {
  color: var(--nav-label-strong);
}

.nav__link:hover::after,
.nav__link:focus-visible::after,
.nav__link[aria-current="page"]::after {
  transform: scaleX(1);
}

/* The navbar CTA is outside any [data-field] section, so it reads the
   data-nav-over tokens directly rather than the ambient --cta-* ones. */
.header__cta {
  display: none;
  --cta-bg: var(--brand-cyan);
  --cta-fg: var(--brand-navy);
  --cta-bg-hover: var(--brand-cyan-lift);
  --cta-ring: transparent;
  --cta-rule: transparent;
  min-height: 2.75rem;
  padding-inline: 1.25rem;
}

html[data-nav-over="aqua"] .header__cta {
  --cta-bg: var(--brand-ink);
  --cta-fg: #ffffff;
  --cta-bg-hover: var(--brand-ink);
  --cta-ring: var(--brand-cyan);
  --cta-rule: var(--brand-cyan);
}

.nav-toggle {
  display: inline-flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  gap: 6px;
  width: 2.75rem;
  height: 2.75rem;
  margin-inline-start: auto;
}

.nav-toggle__bar {
  display: block;
  width: 20px;
  height: 1.5px;
  background: currentColor;
  color: var(--nav-label-strong);
  transition: transform var(--dur-4) var(--ease-detent), width var(--dur-4) var(--ease-detent);
}

.site-header .nav-toggle__bar {
  filter: var(--nav-icon-shadow, none);
}

.mobile-nav .nav-toggle__bar {
  color: #ffffff;
}

.nav-toggle[aria-expanded="true"] .nav-toggle__bar {
  width: 15px;
}

.nav-toggle[aria-expanded="true"] .nav-toggle__bar:nth-child(1) {
  transform: translateY(3.75px) rotate(45deg);
}

.nav-toggle[aria-expanded="true"] .nav-toggle__bar:nth-child(2) {
  transform: translateY(-3.75px) rotate(-45deg);
}

/* --- Mobile drawer: a deliberate design, always its own dark room -------
   (Sec 5.6). It never changes colour with the page underneath it. -------- */

.mobile-nav {
  position: fixed;
  inset: 0;
  z-index: var(--z-nav);
  display: flex;
  flex-direction: column;
  height: 100dvh;
  background: var(--brand-ink);
  overflow-y: auto;
  overscroll-behavior: contain;
  transform: translate3d(0, -100%, 0);
  transition: transform var(--dur-4) var(--ease-mass);
}

.mobile-nav[data-state="open"] {
  transform: none;
}

.mobile-nav__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  min-height: var(--nav-h-sm);
  padding-inline: var(--gutter);
  flex: none;
}

.mobile-nav__nav {
  border-block-start: var(--hairline) solid rgb(255 255 255 / 0.12);
}

.mobile-nav__list {
  display: grid;
}

.mobile-nav__row {
  border-block-end: var(--hairline) solid rgb(255 255 255 / 0.12);
}

.mobile-nav__link {
  position: relative;
  display: grid;
  gap: 0.25rem;
  min-height: 76px;
  padding: var(--space-sm) var(--gutter);
  align-content: center;
  text-decoration: none;
  color: #ffffff;
}

.mobile-nav__link::before {
  content: "";
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0;
  width: 2px;
  background: var(--brand-cyan);
  opacity: 0;
}

.mobile-nav__link[aria-current="page"]::before {
  opacity: 1;
}

.mobile-nav__name {
  font-family: var(--font-display);
  font-size: var(--fs-display-sub);
  font-weight: 400;
  letter-spacing: var(--track-sub);
}

.mobile-nav__desc {
  font-size: var(--fs-caption);
  color: var(--text-ink-muted);
}

.mobile-nav__footer {
  margin-block-start: auto;
  display: grid;
  gap: var(--space-md);
  padding: var(--space-lg) var(--gutter) var(--space-xl);
  flex: none;
}

.mobile-nav__email {
  font-size: var(--fs-body-sm);
  color: var(--text-ink-aqua);
  text-decoration: none;
}

.mobile-nav__cta {
  width: 100%;
  justify-content: center;
  min-height: 56px;
}

@media (max-height: 35rem) {
  .mobile-nav__link {
    min-height: 68px;
  }
}

@media (min-width: 62rem) {
  .site-header__inner {
    min-height: var(--nav-h-md);
  }

  .site-header[data-stuck="true"] .site-header__inner {
    min-height: var(--nav-h-md-condensed);
  }

  .nav {
    display: block;
  }

  .header__cta {
    display: inline-flex;
  }

  .brand__suffix {
    display: inline-block;
  }

  .nav-toggle,
  .mobile-nav {
    display: none;
  }
}

@media (min-width: 75rem) {
  .site-header__inner {
    min-height: var(--nav-h-lg);
  }

  .site-header[data-stuck="true"] .site-header__inner {
    min-height: var(--nav-h-lg-condensed);
  }
}

/* --------------------------------------------------------------------------
   11. Footer
   -------------------------------------------------------------------------- */

.site-footer {
  padding-block: var(--space-2xl) var(--space-lg);
}

.site-footer__grid {
  display: grid;
  gap: var(--space-xl);
}

.site-footer__brand {
  display: grid;
  gap: var(--space-sm);
  max-width: 38ch;
}

.footer-col__heading {
  margin-block-end: var(--space-sm);
  font-family: var(--font-text);
  font-size: var(--fs-body-sm);
  font-weight: 600;
  color: var(--text-muted);
}

.footer-list {
  display: grid;
  font-size: var(--fs-body-sm);
  color: var(--text-secondary);
}

.footer-list li {
  display: flex;
  align-items: center;
  min-height: 2.25rem;
}

/* A comfortable target without opening the visual rhythm up. */
.footer-list a {
  display: inline-flex;
  align-items: center;
  min-height: 2.25rem;
  text-decoration: none;
  transition: color var(--dur-fast) var(--ease-signal);
}

.footer-list a:hover {
  color: var(--text-signal);
}

.site-footer__base {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm) var(--space-lg);
  margin-block-start: var(--space-xl);
  padding-block-start: var(--space-md);
  border-block-start: var(--hairline) solid var(--line-hair);
  font-family: var(--font-text);
  font-size: var(--fs-caption);
  color: var(--text-muted);
}

@media (min-width: 48rem) {
  .site-footer__grid {
    grid-template-columns: 1.5fr 1fr 1fr;
    gap: var(--space-2xl) var(--space-xl);
  }

  .site-footer__brand {
    grid-column: 1 / -1;
  }
}

@media (min-width: 62rem) {
  .site-footer__grid {
    grid-template-columns: 1.6fr 1fr 1fr 1.2fr;
  }

  .site-footer__brand {
    grid-column: auto;
  }
}

/* --------------------------------------------------------------------------
   12. Inquiry overlay and forms
   -------------------------------------------------------------------------- */

/* .inquiry__panel carries data-field="panel" (Sec 4.6): a white ground, not
   a section field. It reads --surface-base / --text-primary / --line-* from
   the [data-field="panel"] tokens block, so nothing here hard-codes a
   colour. The full-screen .inquiry wrapper does NOT carry the field --
   it stays transparent so the page behind the drawer remains visible. */

.inquiry {
  position: fixed;
  inset: 0;
  z-index: var(--z-overlay);
  display: flex;
}

/* Task 4: the page behind the drawer stays visible-but-subdued, not
   effectively hidden -- a much lighter tint of the same navy, carried by a
   substantially heavier blur so legibility (not opacity) does the work of
   separating the drawer from the page. .inquiry__panel is a sibling of
   this element, not a descendant (see .inquiry, above, and the markup) --
   filter never inherits in CSS regardless, but the sibling relationship
   also means no shared ancestor filter can turn into an accidental
   containing block for this fixed-position overlay. */
.inquiry__scrim {
  position: absolute;
  inset: 0;
  width: 100%;
  border: 0;
  background: rgb(6 32 69 / 0.20);
  backdrop-filter: blur(7px) saturate(0.96);
  -webkit-backdrop-filter: blur(7px) saturate(0.96);
  opacity: 0;
  cursor: default;
  transition: opacity var(--dur-4) var(--ease-arrive);
}

/* Fallback for browsers with no backdrop-filter support at all: without
   the blur doing any of the separating work, the flat tint alone has to
   go back to something opaque enough that the drawer's own content never
   competes with the page text behind it. */
@supports not ((backdrop-filter: blur(2px)) or (-webkit-backdrop-filter: blur(2px))) {
  .inquiry__scrim {
    background: rgb(6 32 69 / 0.60);
  }
}

.inquiry[data-state="open"] .inquiry__scrim {
  opacity: 1;
}

.inquiry__panel {
  position: relative;
  display: flex;
  flex-direction: column;
  width: min(36rem, 100%);
  max-height: 100svh;
  margin-inline-start: auto;
  background: var(--surface-base);
  border-block-start: 3px solid var(--brand-navy);
  box-shadow: var(--shadow-overlay);
  overflow-y: auto;
  overscroll-behavior: contain;
  transform: translate3d(100%, 0, 0);
  transition: transform var(--dur-5) var(--ease-mass);
}

.inquiry[data-state="open"] .inquiry__panel {
  transform: none;
}

.inquiry__head {
  position: sticky;
  top: 0;
  z-index: 1;
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-sm);
  padding: var(--space-lg) var(--space-lg) var(--space-md);
  background: var(--surface-base);
}

.inquiry__title {
  font-family: var(--font-display);
  font-size: var(--fs-display-sub);
  font-weight: 400;
}

.inquiry__close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 2.75rem;
  height: 2.75rem;
  border: var(--hairline) solid var(--line-soft);
  border-radius: var(--radius-control);
  color: var(--text-secondary);
  transition: color var(--dur-2) var(--ease-arrive), border-color var(--dur-2) var(--ease-arrive);
}

.inquiry__close:hover {
  color: var(--text-signal);
  border-color: var(--text-signal);
}

.inquiry__close svg {
  width: 1.125rem;
  height: 1.125rem;
}

.inquiry__body {
  padding: 0 var(--space-lg) var(--space-2xl);
}

.inquiry__aside {
  display: grid;
  gap: var(--space-xs);
  margin-block-start: var(--space-xl);
  padding-block-start: var(--space-md);
  border-block-start: var(--hairline) solid var(--line-hair);
  font-size: var(--fs-body-sm);
  color: var(--text-secondary);
}

.inquiry__aside a {
  color: var(--text-signal);
}

.form {
  display: grid;
  gap: var(--space-md);
}

.form__row {
  display: grid;
  gap: var(--space-md);
}

.field-group {
  display: grid;
  gap: var(--space-2xs);
  /* Without this, a field sharing a row with one that has helper text is
     stretched to match it and the two inputs end up different heights. */
  align-content: start;
}

.field-group .field-label {
  color: var(--text-secondary);
}

.field-label__optional {
  font-weight: 400;
  color: var(--text-muted);
}

.control {
  width: 100%;
  padding: 0.8125rem 1rem;
  background: var(--surface-sunken);
  color: var(--text-primary);
  border: var(--hairline) solid var(--line-soft);
  border-radius: var(--radius-unit);
  transition: border-color var(--dur-2) var(--ease-arrive), box-shadow var(--dur-2) var(--ease-arrive);
}

.control::placeholder {
  color: var(--text-muted);
}

.control:hover {
  border-color: var(--line-strong);
}

.control:focus-visible {
  border-color: var(--brand-navy);
  box-shadow: 0 0 0 3px rgb(6 32 69 / 0.16);
}

.control[aria-invalid="true"] {
  border-color: var(--state-error);
}

textarea.control {
  min-height: 8.5rem;
  resize: vertical;
}

/* A flat SVG chevron: one solid stroke colour, no colour ramp of any kind. */
select.control {
  appearance: none;
  padding-inline-end: 2.75rem;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath d='M1 1.5 6 6.5 11 1.5' fill='none' stroke='%23062045' stroke-width='1.4' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-position: calc(100% - 1rem) center;
  background-size: 0.75rem 0.5rem;
  background-repeat: no-repeat;
}

.field-error {
  font-size: var(--fs-caption);
  color: var(--state-error);
}

.field-hint {
  font-size: var(--fs-caption);
  color: var(--text-muted);
}

/* Honeypot. Off screen for people, present in the DOM for bots. */
.field-trap {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

.form__status {
  padding: var(--space-sm);
  border: var(--hairline) solid var(--line-soft);
  border-radius: var(--radius-unit);
  font-size: var(--fs-body-sm);
  color: var(--text-secondary);
}

.form__status[data-kind="success"] {
  border-color: rgb(31 107 69 / 0.4);
  background: rgb(31 107 69 / 0.08);
  color: var(--state-ok);
}

.form__status[data-kind="error"] {
  border-color: rgb(138 43 28 / 0.4);
  background: rgb(138 43 28 / 0.08);
  color: var(--state-error);
}

@media (min-width: 30rem) {
  .form__row--split {
    grid-template-columns: 1fr 1fr;
  }
}

@media (min-width: 48rem) {
  .inquiry__panel {
    border-start-start-radius: var(--radius-vessel);
    border-end-start-radius: var(--radius-vessel);
  }

  .inquiry__head,
  .inquiry__body {
    padding-inline: var(--space-xl);
  }
}

/* --------------------------------------------------------------------------
   13. Technical components
   -------------------------------------------------------------------------- */

/* Section header. Headline over body, never a floating corner paragraph. */
.section-head {
  display: grid;
  gap: var(--space-sm);
  max-width: 46rem;
  margin-block-end: var(--space-xl);
}

/* The heading may run wider than its supporting paragraph. */
.section-head p {
  max-width: 34rem;
}

/* Paired label and value, used for "what it solves" / "where it applies". */
.spec-pair {
  display: grid;
  gap: var(--space-3xs);
}

.spec-pair__value {
  font-size: var(--fs-body-sm);
  color: var(--text-secondary);
  max-width: var(--measure);
}

/* Equipment directory (DESIGN-BRIEF-v3 Sec 7 rewrite). The former
   sticky-rail-over-a-continuous-document pattern (a two-row category rail
   pinned with position: sticky, plus --rail-x indicator math) put the
   rail's own bottom edge over the active heading at 1440 -- fixed
   architecturally, not with a bigger scroll-margin, by removing sticky
   positioning and that whole rail component from this section entirely. A
   pill tablist (never sticky, horizontally scrollable) selects
   one .plate-card--feature panel at a time; a stepper moves between them.
   Every panel renders visible in the generated HTML -- the no-JS fallback,
   all fourteen stacked with no rail -- and initTechTabs() applies `hidden`
   to all but the active one at init, never in the markup. */
.tech-directory {
  display: grid;
  gap: var(--space-lg);
}

.tech-tablist {
  display: flex;
  flex-wrap: nowrap;
  gap: var(--space-2xs);
  margin-inline: calc(var(--gutter) * -1);
  padding-inline: var(--gutter);
  padding-block-end: var(--space-2xs);
  overflow-x: auto;
  scrollbar-width: none;
}

.tech-tablist::-webkit-scrollbar {
  display: none;
}

@media (min-width: 64rem) {
  .tech-tablist {
    flex-wrap: wrap;
    overflow-x: visible;
    margin-inline: 0;
    padding-inline: 0;
  }
}

.tech-tab {
  flex: none;
  padding: 0.5rem 0.875rem;
  background: none;
  border: var(--hairline) solid var(--line-strong);
  border-radius: var(--radius-pill);
  font-family: var(--font-text);
  font-size: var(--fs-body-sm);
  font-weight: 500;
  color: var(--text-secondary);
  white-space: nowrap;
  cursor: pointer;
  transition:
    background-color var(--dur-2) var(--ease-arrive),
    border-color var(--dur-2) var(--ease-arrive),
    color var(--dur-2) var(--ease-arrive);
}

.tech-tab:hover,
.tech-tab:focus-visible {
  border-color: var(--signal);
  color: var(--text-primary);
}

.tech-tab[aria-selected="true"] {
  background: var(--signal);
  border-color: var(--signal);
  color: var(--signal-ink);
}

.tech-panels {
  display: grid;
}

.tech-panel {
  display: grid;
}

/* .tech-panel__plate, .tech-panel__plate .plate-card__media and
   .tech-panel__sample's own layout now live in the "23. Equipment section"
   fenced block (full-bleed media card + solid overlay panel, Task B of the
   services-page redesign) further down this file -- this used to be a
   media + content pair built from the shared .plate-card component; that
   markup no longer exists on this page. */

.tech-stepper {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-md);
  margin-block-start: var(--space-md);
}

.tech-stepper__count {
  min-width: 5.5rem;
  font-family: var(--font-text);
  font-size: var(--fs-body-sm);
  font-weight: 600;
  letter-spacing: 0.06em;
  color: var(--text-secondary);
  text-align: center;
}

.tech-directory__count {
  font-size: var(--fs-caption);
  color: var(--text-muted);
}

/* .spec-grid: the .chips / .chip replacement (creative-direction-v2 Sec
   2.5 / 11.3). Chips read as filter UI; a technical taxonomy reads as a
   document instead when it is plain .spec-term entries in a gap:1px grid,
   the grid's own background producing the hairlines -- no border, no
   pill, no fill is declared on any individual term. */
/* HIGH H9 + L6 fix. This grid used to draw its hairlines by painting the
   whole container --line-hair and letting each real .spec-term punch a
   --surface-base "hole" in it, with the 1px gap left showing as a rule.
   That works for a full grid, but auto-fill computes its column count from
   the container width regardless of how many actual .spec-term items
   exist -- an incomplete trailing row (Level/Temperature/Pressure on
   index, the Manufacturers grid on services, the six-industry chip grid
   on about, whichever one lands short of a full row) leaves a genuinely
   empty implicit cell with no element in it to punch a hole, so the
   container's flat --line-hair fill showed through as a solid tinted
   rectangle -- read as a failed load, not empty space. Fixed by flipping
   which side owns the fill: the container is transparent (nothing to
   bleed through where no cell exists) and each real cell draws its own
   trailing/bottom hairline instead. */
.spec-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
  gap: var(--hairline);
  background: transparent;
  border-block-start: var(--hairline) solid var(--line-hair);
}

/* Task 2: every cell centred on both axes, symmetric padding, a shared
   min-height rhythm for single- and two-line entries alike. Flex rather
   than grid-in-grid -- one cell, one line-wrappable text node, no extra
   markup needed. The hairline system above this rule (container
   transparent, each cell punches its own trailing/bottom rule) is
   untouched: background/border-inline-end/border-block-end stay exactly
   as they were, only the box's internal alignment changes. */
.spec-grid .spec-term {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 3.5rem;
  padding: 0.75rem 1rem;
  text-align: center;
  background: var(--surface-base);
  border-inline-end: var(--hairline) solid var(--line-hair);
  border-block-end: var(--hairline) solid var(--line-hair);
}

.spec-grid--vendors {
  grid-template-columns: repeat(auto-fill, minmax(13rem, 1fr));
}

/* --------------------------------------------------------------------------
   14. Home sections
   Field run (creative-direction-v2 Sec 4.2): dark, dark, aqua, aqua, aqua,
   dark, dark, aqua, aqua, dark.
   -------------------------------------------------------------------------- */

/* --- Hero: scroll-scrubbed video, copy in the lower-left quadrant --------
   (creative-direction-v2 Sec 6). No scrim of any kind: legibility comes from
   a single flat translucent plate plus a dark-graded lower-left composition. */

/* BLOCKER 1 fix, the other half. .site-header is position:sticky (Sec 5.1
   mandates it, not fixed) and a sticky element still occupies its own box
   in normal flow at rest -- it does not overlay whatever follows it in the
   DOM. Without this, "transparent nav" is invisible in practice: nothing
   shows through it, because the hero video starts only after the header's
   own height, not behind it. Pulling the hero up by exactly the header's
   current height (mirrored from the .site-header__inner breakpoints
   below) lets the (z-index: 200) header render on top of the video from
   the very first frame instead. This shifts .hero__scroll, the element
   initHero() measures for scroll progress, by a constant amount equal to
   the nav height -- a few dozen pixels against a multi-thousand-pixel
   scroll range, negligible against the total and not a change to the
   seek logic itself. */
.hero {
  margin-block-start: calc(var(--nav-h-sm) * -1);
}

@media (min-width: 62rem) {
  .hero {
    margin-block-start: calc(var(--nav-h-md) * -1);
  }
}

@media (min-width: 75rem) {
  .hero {
    margin-block-start: calc(var(--nav-h-lg) * -1);
  }
}

.hero__scroll {
  position: relative;
  height: 320vh;
}

.hero__stage {
  position: sticky;
  top: 0;
  height: 100dvh;
  min-height: 100dvh;
  overflow: hidden;
  background: var(--brand-ink);
}

.hero__media {
  position: absolute;
  inset: 0;
  /* Beat 3 (site.js): a slow push-in as the stage settles, so the section
     join lands on a composed frame instead of a wipe. */
  transform: scale(calc(1 + 0.035 * var(--settle, 0)));
  transform-origin: 50% 55%;
}

/* Flat colour-grade tint on the footage (not a panel behind the text) for
   headline/CTA legibility. Deepens slightly as the scrub settles. Never a
   gradient. */
.hero__media::after {
  content: "";
  position: absolute;
  inset: 0;
  background-color: rgb(6 32 69 / calc(0.34 + 0.2 * var(--settle, 0)));
  pointer-events: none;
}

/* HIGH H1 + MEDIUM M3: video and poster share one object-position so
   framing never jumps when the video replaces the poster. The hero card
   no longer covers the lower half of the stage (Sec "target layout" fix),
   so the full 100dvh aperture is visible now, not the old ~397px band --
   the previous 58%/62% downward bias was tuned for that cramped aperture
   and pushed the framing too low once the whole frame is on screen. A
   mild downward bias is kept only to favour the plant / substation over
   empty sky at the very top of the frame, re-tuned by screenshot at
   375/768/1440 against the full viewport. */
.hero__video,
.hero__poster-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 52%;
}

/* Task 1: no more opaque card -- the copy sits directly over the video, a
   plain positioned block anchored lower-left. Beat 2 (site.js) drives
   --copy (1 -> 0) so the copy exits upward and fades before the stage
   settles; measure/max-width now live on .hero__copy (below), not here. */
.hero__field {
  position: absolute;
  inset-inline-start: var(--gutter);
  inset-inline-end: var(--gutter);
  /* Cleared of the progress bar (pinned to the stage's own bottom edge,
     below) by an explicit gap on top of the gutter inset. */
  inset-block-end: calc(var(--gutter) + 34px);
  z-index: 2;
  opacity: var(--copy, 1);
  transform: translate3d(0, calc((1 - var(--copy, 1)) * -40px), 0);
  will-change: opacity, transform;
}

/* Once faded out (Beat 2) the copy must not remain a click/tab target
   sitting invisibly over the footage. Set from site.js, not a brittle
   attribute-substring selector. */
.hero__stage[data-copy-gone="true"] .hero__field {
  visibility: hidden;
  pointer-events: none;
}

/* HIGH fix, part 2: height-based tiers, hero-scoped only. A short viewport
   (tablet landscape, or any laptop with a short browser window) leaves far
   less vertical room than the type/padding/gap were tuned for, regardless
   of width. These reduce the hero card's own type size, padding and copy
   gap so the content genuinely fits within the max-height ceiling above --
   scoped to .hero__title / .hero__field / .hero__copy only, never touching
   the global --fs-display-hero token or any other page's heading scale. */
@media (max-height: 44rem) {
  .hero__title {
    font-size: clamp(1.5rem, 1.05rem + 1.9vw, 2.25rem);
  }

  .hero__copy {
    gap: var(--space-sm);
  }
}

@media (max-height: 38rem) {
  .hero__title {
    font-size: clamp(1.3rem, 0.95rem + 1.4vw, 1.85rem);
  }

  .hero__copy {
    gap: var(--space-2xs);
  }
}

/* --- Hero progress: JS-driven scrub indicator. Now that the bottom edge
   of the stage is clear of any opaque plate, this goes back to the
   bottom edge of .hero__stage itself (the nearest positioned ancestor)
   rather than the top of the copy card -- simpler, and it can never be
   overlapped by the card because the card's own inset-block-end (above)
   leaves an explicit gap above this bar. z-index keeps it topmost at its
   own coordinates. Transform-only fill -- never width. ------------------ */
.hero-progress {
  position: absolute;
  inset-inline: 0;
  inset-block-end: 0;
  height: 2px;
  z-index: 3;
  background: var(--line-hair);
  opacity: 0;
  transition: opacity var(--dur-3) var(--ease-arrive);
}

.hero-progress[data-active="true"] {
  opacity: 1;
}

.hero-progress__bar {
  display: block;
  width: 100%;
  height: 100%;
  background: var(--brand-cyan);
  transform: scaleX(var(--hero-p, 0));
  transform-origin: left center;
}

.hero__copy {
  position: relative;
  display: grid;
  gap: var(--space-md);
  color: #ffffff;
  /* Task 1: no card padding -- the copy sits directly over the video. The
     measure lives here, not on .hero__field, so it can bleed to the full
     gutter-to-gutter width on narrow screens. */
  max-width: 100%;
  will-change: transform;
}

@media (min-width: 48rem) and (max-width: 63.999rem) {
  .hero__copy {
    max-width: min(40rem, 88vw);
  }
}

@media (min-width: 64rem) {
  .hero__copy {
    max-width: min(52rem, 74vw);
  }
}

.hero__marker {
  color: #ffffff;
  text-shadow: 0 1px 2px rgb(6 32 69 / 0.55), 0 2px 18px rgb(6 32 69 / 0.35);
}

/* The headline relies on natural wrapping plus non-breaking spaces at
   specific junctions that would otherwise strand a short word ("We
   engineer,", "supply and", "and control") or split a compound term
   ("industrial electrical,"). See the h1 in index.html.

   Task 1: no longer trapped in a 32rem card, so the title steps up one
   size and can balance across its full measure. Restrained text-shadow
   (marker/title/lead all carry it) is legitimate here only because the
   ground is the scrubbed video, a media ground with variable luminance --
   not used on the flat About/Services heroes. */
.hero__title {
  font-family: var(--font-display);
  font-size: clamp(2.25rem, 1.4rem + 3.4vw, 3.6rem);
  line-height: 1.16;
  letter-spacing: var(--track-hero);
  /* Switzer's real 500 master, not Quantify's single static weight. */
  font-weight: 500;
  max-width: none;
  text-wrap: pretty;
  text-shadow: 0 1px 2px rgb(6 32 69 / 0.55), 0 2px 18px rgb(6 32 69 / 0.35);
}

.hero__lead {
  color: rgb(255 255 255 / 0.82);
  text-shadow: 0 1px 2px rgb(6 32 69 / 0.55), 0 2px 18px rgb(6 32 69 / 0.35);
}

.hero__actions {
  margin-block-start: var(--space-2xs);
}

@media (max-width: 47.999rem) {
  .hero__scroll {
    height: 220vh;
  }
}

/* --- Positioning claim ---------------------------------------------------- */

.claim {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--space-lg);
  align-items: stretch;
}

/* The rule spans both stacked rows (mobile: pull-quote over aside); both
   text blocks share the single content column until the 64rem split. */
.claim > .trace-rule--v {
  grid-row: 1 / -1;
}

.claim__text,
.claim__aside {
  grid-column: 2;
}

.claim__text {
  font-family: var(--font-display);
  font-size: var(--fs-display-sub);
  font-weight: 400;
  line-height: 1.28;
  letter-spacing: var(--track-sub);
  max-width: 40ch;
  color: var(--text-primary);
}

@media (min-width: 48rem) {
  .claim {
    gap: var(--space-xl);
  }
}

/* Editorial split (about.html "positioning claim" only): the pull-quote and
   its supporting paragraphs read as two columns rather than one stacked
   block, each at its own measure. */
@media (min-width: 64rem) {
  .claim {
    grid-template-columns: auto minmax(0, 1fr) minmax(0, 22rem);
    gap: var(--space-2xl);
    align-items: start;
  }

  .claim > .trace-rule--v {
    grid-row: 1;
  }

  .claim__text {
    grid-column: 2;
    grid-row: 1;
    padding-inline-end: var(--space-xl);
  }

  .claim__aside {
    grid-column: 3;
    grid-row: 1;
  }
}

/* --- Capability index: asymmetric bento, exactly four cells --------------- */

.capabilities {
  display: grid;
  gap: var(--grid-gap);
}

/* MEDIUM M2 fix: this card was --surface-raised (#b2e6f0 on the aqua field)
   with a #b2e6f0 border -- close enough to the #98deeb ground to read as
   neither a raised surface nor nothing, so it looked like a stray outline
   rather than a deliberate card, and #b2e6f0 was a sixth colour. White is
   already in the five-colour palette: a confident, unambiguous surface on
   aqua, no border needed to separate it from the ground. */
.capability {
  display: grid;
  gap: var(--space-sm);
  align-content: start;
  padding: var(--space-md);
  background: #ffffff;
  border-radius: var(--radius-vessel);
  box-shadow: var(--shadow-raise, none);
}

/* Two of the four capability cards carry a photograph (the manifest
   supplies exactly two 4:5 stills for this slot); M6 Scroll-Linked Image
   Movement runs on both, capped well inside the 3-per-page limit.
   MEDIUM 3 (shape pass): --radius-plate, not --radius-vessel -- this is
   feature imagery (the token's own stated purpose), not a card/panel.
   Uniform on all four corners rather than an asymmetric mask: the card's
   own padding insets the figure equally on every side, so there is no
   single "outer" edge for an asymmetric cut to favour. */
.capability__figure {
  position: relative;
  overflow: hidden;
  aspect-ratio: 4 / 5;
  margin-block-end: var(--space-2xs);
  border-radius: var(--radius-plate);
}

.capability__glyph {
  width: 3rem;
  height: 3rem;
  color: var(--text-signal);
}

.capability__list {
  display: grid;
  gap: var(--space-3xs);
  font-size: var(--fs-body-sm);
  color: var(--text-secondary);
}

.capability__list li {
  position: relative;
  padding-inline-start: var(--space-sm);
}

.capability__list li::before {
  content: "";
  position: absolute;
  inset-inline-start: 0;
  top: 0.72em;
  width: 0.5rem;
  height: var(--hairline);
  background: var(--signal);
}

@media (min-width: 48rem) {
  .capabilities {
    grid-template-columns: repeat(6, 1fr);
  }

  .capability {
    padding: var(--space-lg);
  }

  .capability:nth-child(1) {
    grid-column: span 3;
  }
  .capability:nth-child(2) {
    grid-column: span 3;
  }
  .capability:nth-child(3) {
    grid-column: span 2;
  }
  .capability:nth-child(4) {
    grid-column: span 4;
  }
}

/* --- Signal story: five stages of one measurement ------------------------- */

.signal-rail {
  display: grid;
  gap: var(--space-md);
  /* .signal-node__travel sweeps in from translate3d(-24px,0,0), off the
     inline edge of the first node -- clip here (the owning row), not
     overflow-x:hidden on body, and use clip rather than hidden so this
     never becomes a scroll container or breaks a sticky ancestor. */
  overflow-x: clip;
}

.signal-node {
  position: relative;
  display: grid;
  gap: var(--space-2xs);
  align-content: start;
  padding-block-start: var(--space-md);
  border-block-start: var(--hairline) solid var(--line-soft);
}

/* M4 Signal Travel (creative-direction-v2 Sec 7.3, Home Sec 4 only, once
   per page). The rule itself stays a neutral --line-strong path -- it is
   .signal-node__travel, a 24px --zone-signal segment, that carries the
   colour once as it travels the path and terminates at the node, which
   then runs M3 (already wired via [data-reveal-state="in"] .node-marker).
   This is also the reduced-motion end state the spec calls for verbatim:
   "the full path draws at --zone-line-strong and all five nodes are in
   their active state" -- true unconditionally, since the travel segment
   is display:none under reduced motion (Section 18) rather than the path
   itself changing colour. */
.signal-node::before {
  content: "";
  position: absolute;
  inset-block-start: -1px;
  inset-inline-start: 0;
  width: 2.5rem;
  height: 2px;
  background: var(--line-strong);
}

.signal-node__travel {
  position: absolute;
  inset-block-start: -1px;
  inset-inline-start: 0;
  width: 24px;
  height: 2px;
  background: var(--signal);
  opacity: 0;
  transform: translate3d(-24px, 0, 0);
}

[data-reveal-state="in"] .signal-node__travel {
  animation: signal-travel var(--dur-6) var(--ease-travel) both;
}

@keyframes signal-travel {
  0% {
    opacity: 1;
    transform: translate3d(-24px, 0, 0);
  }
  85% {
    opacity: 1;
  }
  100% {
    opacity: 0;
    transform: translate3d(2.5rem, 0, 0);
  }
}

.signal-node__name {
  font-family: var(--font-display);
  font-size: var(--fs-display-sub);
  font-weight: 400;
  letter-spacing: var(--track-sub);
}

.signal-node__body {
  max-width: 34ch;
  font-size: var(--fs-body-sm);
  color: var(--text-secondary);
}

@media (min-width: 62rem) {
  .signal-rail {
    grid-template-columns: repeat(5, 1fr);
    gap: var(--grid-gap);
  }
}

/* --- Selected services: a read-down index, not a card grid ---------------- */

.service-index {
  border-block-start: var(--hairline) solid var(--line-soft);
}

.service-index__row {
  display: grid;
  gap: var(--space-2xs);
  padding-block: var(--space-md);
  border-block-end: var(--hairline) solid var(--line-hair);
}

.service-index__title {
  font-family: var(--font-display);
  font-size: var(--fs-display-sub);
  font-weight: 400;
  letter-spacing: var(--track-sub);
  line-height: var(--lh-sub);
}

.service-index__body {
  max-width: 52ch;
  color: var(--text-secondary);
}

@media (min-width: 48rem) {
  .service-index__row {
    grid-template-columns: 5fr 7fr;
    gap: var(--space-lg);
    align-items: baseline;
  }
}

/* --- Industry strip: horizontal scroll snap ------------------------------- */

.industry-strip {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(15rem, 19rem);
  gap: var(--grid-gap);
  margin-inline: calc(var(--gutter) * -1);
  padding-inline: var(--gutter);
  padding-block-end: var(--space-md);
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-padding-inline: var(--gutter);
  overscroll-behavior-x: contain;
  /* The native scrollbar this rail paints reads as a page-level overflow
     defect, not an intentional snap rail -- hidden here (::-webkit rule
     below), a right-edge fade takes its place as the "there is more"
     affordance instead. */
  scrollbar-width: none;
}

.industry-strip::-webkit-scrollbar {
  display: none;
}

@media (min-width: 48rem) {
  .industry-strip {
    /* Fade, not a clipped focus ring: mask-image only touches the strip's
       own right edge, well clear of any card's focus box-shadow, which is
       drawn on the card element itself, not the strip. */
    mask-image: linear-gradient(to right, #000 calc(100% - 3rem), transparent);
  }
}

.industry-strip:focus-visible {
  outline: none;
  box-shadow: var(--ring);
  border-radius: var(--radius-unit);
}

/* Desktop: room for all six cards at once, so the scroller (and its fade)
   is dropped entirely rather than kept as an unnecessary rail. MEDIUM 2
   fix: a plain repeat(3, 1fr) here read as six interchangeable boxes (the
   basic square-card grid this project's Industries requirement rules
   out). A 12-unit track with per-card spans keeps the same two-row/six-
   card structure and all six entries verbatim, but breaks the equal-tile
   rhythm -- row one runs wide/mid/narrow (5/4/3 units), row two mirrors
   it narrow/mid/wide (3/4/5), so no two cards in the grid share a width
   and the pair of rows reads as one deliberate composition, not a
   stamped-out 3x2. Text-only, no imagery added -- the six industry
   photographs already appear once each on services.html and about.html,
   their site-wide limit of two. */
@media (min-width: 62rem) {
  .industry-strip {
    grid-auto-flow: row;
    grid-template-columns: repeat(12, 1fr);
    /* Reset the bleed-to-viewport-edge treatment: it exists only to let
       the scroller run under the gutter while active. A static wrapping
       grid keeps the normal container width instead, or this pushes the
       strip 2x gutter wider than its container -- an independent overflow
       source on top of the grid-auto-flow typo this replaces. */
    margin-inline: 0;
    padding-inline: 0;
    padding-block-end: 0;
    overflow-x: visible;
    scroll-snap-type: none;
    scroll-padding-inline: 0;
    mask-image: none;
  }

  .industry-card:nth-child(1) {
    grid-column: span 5;
  }

  .industry-card:nth-child(2) {
    grid-column: span 4;
  }

  .industry-card:nth-child(3) {
    grid-column: span 3;
  }

  .industry-card:nth-child(4) {
    grid-column: span 3;
  }

  .industry-card:nth-child(5) {
    grid-column: span 4;
  }

  .industry-card:nth-child(6) {
    grid-column: span 5;
  }
}

.industry-card {
  display: grid;
  gap: var(--space-2xs);
  align-content: start;
  padding: var(--space-md);
  background: var(--surface-raised);
  border: var(--hairline) solid var(--line-hair);
  /* MEDIUM 2 fix: the signal-cyan top border used to run identically
     across all six cards -- one of the repetitions that made the grid
     read as templated. Now it is the first card's own permanent mark (a
     "lead" entry, matching the plate-card top-edge-signal-on-hover motif
     elsewhere on the site) plus a hover/focus reveal on every card, so
     the accent still greets the reader immediately without repeating
     itself six times at rest. */
  border-block-start: 2px solid var(--line-hair);
  border-radius: var(--radius-vessel);
  scroll-snap-align: start;
  transition: border-color var(--dur-3) var(--ease-arrive);
}

.industry-card:first-child {
  border-block-start-color: var(--signal);
}

.industry-card:hover,
.industry-card:focus-within {
  border-block-start-color: var(--signal);
}

.industry-card__name {
  font-family: var(--font-text);
  font-size: var(--fs-title-module);
  font-weight: 600;
}

.industry-card__body {
  font-size: var(--fs-body-sm);
  color: var(--text-secondary);
}

/* --- Technology teaser ---------------------------------------------------- */

.tech-groups {
  display: grid;
  gap: var(--space-lg);
}

.tech-group {
  display: grid;
  gap: var(--space-xs);
  padding-block-start: var(--space-md);
  border-block-start: var(--hairline) solid var(--line-soft);
}

@media (min-width: 48rem) {
  .tech-groups {
    grid-template-columns: repeat(2, 1fr);
    gap: var(--space-lg) var(--space-xl);
  }
}

/* --- Why SIMSAN: editorial alternating rhythm, not three equal cards ------ */

.capability__title {
  font-family: var(--font-display);
  font-size: var(--fs-display-sub);
  font-weight: 400;
  letter-spacing: var(--track-sub);
}

.why {
  display: grid;
  gap: var(--space-xl);
}

.why__item {
  display: grid;
  gap: var(--space-2xs);
  align-content: start;
}

/* Lead (01): filled plate, signal-coloured top rule. */
.why__item--lead {
  background: var(--surface-raised);
  border-radius: var(--radius-vessel);
  padding: var(--space-lg);
  border-block-start: 2px solid var(--signal);
}

/* 02 / 03: no fill, just a rule and the numeral -- sits directly on the
   aqua field. Deliberate asymmetry with the lead item, not an oversight. */
.why__item:not(.why__item--lead) {
  border-inline-start: 2px solid var(--brand-navy);
  padding-inline-start: var(--space-md);
}

.why__num {
  font-family: var(--font-display);
  font-size: var(--fs-body-sm);
  letter-spacing: var(--track-sub);
  color: var(--text-secondary);
}

.why__item--lead .why__num {
  color: var(--signal);
}

.why__title {
  font-family: var(--font-display);
  font-size: var(--fs-display-sub);
  font-weight: 400;
  letter-spacing: var(--track-sub);
}

.why__item--lead .why__title {
  font-size: calc(var(--fs-display-sub) * 1.15);
}

.why__media {
  border-radius: var(--radius-vessel);
  overflow: hidden;
}

.why__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.why__media:nth-of-type(1) {
  aspect-ratio: 3 / 2;
}

.why__media:nth-of-type(2) {
  aspect-ratio: 2 / 1;
}

.why__media:nth-of-type(3) {
  aspect-ratio: 9 / 4;
}

@media (min-width: 62rem) {
  .why {
    grid-template-columns: repeat(12, 1fr);
    align-items: center;
    gap: var(--space-2xl) var(--grid-gap);
  }

  /* Row 1: principle 01 (wide feature panel) + image A. */
  .why__item:nth-of-type(1) {
    grid-column: 1 / span 6;
    grid-row: 1;
  }
  .why__media:nth-of-type(1) {
    grid-column: 7 / span 6;
    grid-row: 1;
  }

  /* Row 2: image B + principle 02. Centre-aligned against its paired
     image (Sec: dead-band fix), not bottom-locked with an offset. */
  .why__media:nth-of-type(2) {
    grid-column: 1 / span 5;
    grid-row: 2;
  }
  .why__item:nth-of-type(2) {
    grid-column: 7 / span 6;
    grid-row: 2;
  }

  /* Row 3: principle 03 + image C (landscape). */
  .why__item:nth-of-type(3) {
    grid-column: 1 / span 5;
    grid-row: 3;
  }
  .why__media:nth-of-type(3) {
    grid-column: 6 / span 7;
    grid-row: 3;
  }
}

/* --- Inquiry band --------------------------------------------------------- */

.cta-band__grid {
  display: grid;
  gap: var(--space-lg);
  align-items: end;
}

.cta-band__title {
  max-width: 18ch;
}

@media (min-width: 62rem) {
  .cta-band__grid {
    grid-template-columns: 1fr auto;
    gap: var(--space-2xl);
  }
}

/* --------------------------------------------------------------------------
   15. Services sections
   -------------------------------------------------------------------------- */

/* HIGH H7 fix: Services and About share this exact class, so they were
   never actually unequal in the CSS -- but the top inset both used (max
   5.5rem/88px at 1440) fell short of the ~110px the brief calls for, and
   read as cramped against the header. Raised so the page-hero top inset
   lands at ~101px at 1440 (7vw, no longer capped below it) on both pages. */
.page-hero {
  padding-block: clamp(3rem, 7vw, 6.875rem) clamp(2.5rem, 6vw, 4.5rem);
}

/* MEDIUM M2 fix: on about.html (the one light aqua-field page-hero) the
   header is transparent at scroll 0, but the hero section started below
   it, so the body's --field-ink showed through as a dark band and the
   transparent nav read as a solid dark bar. Same treatment .hero already
   uses on index.html -- pull the section up under the header by the
   header's own height and add it back as top padding, scoped to the aqua
   variant only (services.html's page-hero is data-field="dark" and is not
   touched). initNavAdaptive() already reads data-field to resolve nav
   contrast, so the links correctly switch to the dark/navy treatment here. */
.page-hero[data-field="aqua"] {
  margin-block-start: calc(var(--nav-h-sm) * -1);
  padding-block-start: calc(clamp(3rem, 7vw, 6.875rem) + var(--nav-h-sm));
}

@media (min-width: 62rem) {
  .page-hero[data-field="aqua"] {
    margin-block-start: calc(var(--nav-h-md) * -1);
    padding-block-start: calc(clamp(3rem, 7vw, 6.875rem) + var(--nav-h-md));
  }
}

@media (min-width: 75rem) {
  .page-hero[data-field="aqua"] {
    margin-block-start: calc(var(--nav-h-lg) * -1);
    padding-block-start: calc(clamp(3rem, 7vw, 6.875rem) + var(--nav-h-lg));
  }
}

.page-hero__grid {
  display: grid;
  gap: var(--space-md);
  max-width: var(--container-narrow);
}

/* --------------------------------------------------------------------------
   Shared carousel core (System 1 solutions, System 4 industries).
   Snap rail + hidden native scrollbar + prev/next + pointer drag + keyboard.
   Never hijacks vertical page scroll: no preventDefault on wheel/touchmove,
   no pinning. Touch swipe is native scroll-snap, not reimplemented.
   -------------------------------------------------------------------------- */

.carousel {
  position: relative;
  /* Grid-item blowout guard: the industries carousel sits inside
     .industry-directory (display: grid at every width, not just >=900px).
     A grid item's default min-width is auto (its content's min-content
     size), so without this the wide, non-wrapping .carousel__rail content
     was sizing its own grid column to max-content and pushing the whole
     document past the viewport at narrow widths -- not an internal
     scroller, a document-level overflow. min-width: 0 is a no-op in the
     solutions carousel's plain block context (.shell). */
  min-width: 0;
}

.carousel__rail {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(16.5rem, 21rem);
  gap: var(--grid-gap);
  margin-inline: calc(var(--gutter) * -1);
  padding-inline: var(--gutter);
  padding-block-end: var(--space-sm);
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x mandatory;
  scroll-padding-inline: var(--gutter);
  overscroll-behavior-x: contain;
  scrollbar-width: none;
  touch-action: pan-y pinch-zoom;
}

.carousel__rail::-webkit-scrollbar {
  display: none;
}

/* MEDIUM M1 fix: same right-edge fade the industry strip already uses, so
   a card hard-cut at the viewport edge reads as "more content", not a
   layout defect. The mask only touches the rail's own right edge -- clear
   of a focused card's own focus box-shadow, drawn on the card element. */
@media (min-width: 48rem) {
  .carousel__rail {
    mask-image: linear-gradient(to right, #000 calc(100% - 3rem), transparent);
  }
}

.carousel__rail.is-dragging {
  scroll-snap-type: none;
  cursor: grabbing;
}

.carousel__item {
  scroll-snap-align: start;
}

.carousel__nav {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-2xs);
  margin-block-start: var(--space-sm);
}

.carousel__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.75rem;
  height: 2.75rem;
  padding: 0;
  background: none;
  border: var(--hairline) solid var(--line-strong);
  border-radius: var(--radius-control);
  color: var(--text-primary);
  cursor: pointer;
  transition:
    border-color var(--dur-2) var(--ease-arrive),
    color var(--dur-2) var(--ease-arrive),
    opacity var(--dur-2) var(--ease-arrive);
}

.carousel__btn svg {
  width: 1rem;
  height: 1rem;
}

.carousel__btn--prev svg {
  transform: scaleX(-1);
}

.carousel__btn:hover {
  border-color: var(--signal);
  color: var(--text-signal);
}

.carousel__btn[disabled] {
  opacity: 0.4;
  cursor: not-allowed;
}

/* --- Solutions carousel (System 1) ---------------------------------------- */

.seg-filter {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2xs);
}

.seg-filter__btn {
  padding: 0.5rem 0.875rem;
  background: none;
  border: var(--hairline) solid var(--line-strong);
  border-radius: var(--radius-pill);
  font-family: var(--font-text);
  font-size: var(--fs-body-sm);
  font-weight: 500;
  color: var(--text-secondary);
  cursor: pointer;
  transition:
    background-color var(--dur-2) var(--ease-arrive),
    border-color var(--dur-2) var(--ease-arrive),
    color var(--dur-2) var(--ease-arrive);
}

.seg-filter__btn:hover {
  border-color: var(--signal);
}

.seg-filter__btn[aria-pressed="true"] {
  background: var(--signal);
  border-color: var(--signal);
  color: var(--signal-ink);
}

/* Solution cards are now .plate-card--flush (Sec 2 / Task B): the card
   itself, its title, text and eyebrow all come from that shared component.
   .solution-card only remains as the filter hook -- site.js toggles
   [data-filter-hidden] on the same element rather than removing/re-adding
   cards, so no listener is ever attached twice. */
.solution-card[data-filter-hidden] {
  display: none;
}

/* Task 3: the carousel row (.carousel__rail, display: grid, grid-auto-flow:
   column) stretches every card in the row to the tallest card's height by
   default, but .plate-card__body only ever sized itself to its own content
   (align-content: start on .plate-card) -- the leftover space trailed
   below the Details button as dead area. Scoped to .solution-card only, so
   the shared .plate-card--flush component (industry cards, tech-panel
   samples) is untouched.
   1. .solution-card.plate-card switches align-content to stretch (its
      initial value) so .plate-card__body's single grid row -- and so
      .plate-card__body itself, via the default align-items: stretch --
      actually fills the card's full height instead of sitting top-packed.
   2. .plate-card__body becomes a column flex box (same visual stacking as
      its previous single-column grid, gap included) so percentage/auto
      distribution works inside it.
   3. margin-block-start: auto on .plate-card__foot then consumes whatever
      space is left after the eyebrow/title/text, pinning Details to the
      card's own bottom edge -- the dead area is absorbed there instead of
      trailing past the button. */
.solution-card.plate-card {
  align-content: stretch;
}

/* .solution-card.plate-card--flush (both classes -- specificity 0,3,0),
   not the plain .solution-card descendant selector: .plate-card--flush
   .plate-card__body (below, Sec "--flush: no media...") sets its own
   --space-lg padding at equal 0,2,0 specificity but later in the
   cascade, so without the extra class here that later rule silently won
   and the padding trim below never actually applied. */
.solution-card.plate-card--flush .plate-card__body {
  display: flex;
  flex-direction: column;
  /* Trimmed from the flush default's --space-lg (2rem): that padding, on
     top of the top-packed content above, was most of what pushed a plain
     card past 400px tall. Still comfortable -- a continuous clamp rather
     than a single breakpoint jump, so tablet/mobile (768/390) get their
     own intentional value instead of the desktop number just scaling
     down verbatim. */
  padding: clamp(1.125rem, 0.85rem + 1vw, 1.5rem);
}

.solution-card .plate-card__foot {
  margin-block-start: auto;
}

/* Safety valve, not a general-purpose clamp: at 5 lines only the single
   longest of the eleven current descriptions ("Harsh Process Conditions")
   still trims -- every other card's full sentence survives untouched. It
   still caps how far one future outlier could grow the whole (equal-
   height) row without silently truncating normal copy the way a tighter
   clamp did. */
.solution-card .plate-card__text {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  /* 5, not 4: measured against the real copy deck, a clamp of 4 was
     actively cutting two of the eleven descriptions (one of them,
     "Harsh Process Conditions", down to half its sentences) -- meaning
     lost, not a safety valve. 5 lines clears every description except
     that one outlier untouched; only it still trims, which is what the
     valve is for. */
  -webkit-line-clamp: 5;
  overflow: hidden;
}

/* Responsive height ceiling/floor: keeps the row in the ~290-330px target
   band at desktop while never forcing a card shorter than its own content
   needs at any width -- min-height, not height, so a genuinely long title
   + description can still grow the row. */
.solution-card.plate-card {
  min-height: clamp(15.5rem, 12rem + 6vw, 20rem);
}

/* --- Engineering lifecycle: the Process Line motif, once per site --------- */

.process {
  --fill: 0;
  position: relative;
  display: grid;
  gap: var(--space-lg);
  padding-inline-start: var(--space-lg);
}

.process__track {
  position: absolute;
  inset-block: 0.75rem 0;
  inset-inline-start: 0;
  width: 2px;
  background: var(--line-soft);
}

.process__fill {
  position: absolute;
  inset: 0;
  background: var(--signal);
  transform: scaleY(var(--fill));
  transform-origin: top;
  transition: transform var(--dur-base) var(--ease-instrument);
}

/* A stage is a real <button>: click, Enter/Space, Tab all work with no
   ARIA invented. site.js additionally binds Left/Right/Home/End and touch
   swipe on the .process group, and advances the fill + active state. */
.process__stage {
  position: relative;
  display: grid;
  gap: var(--space-2xs);
  width: 100%;
  padding: 0;
  background: none;
  border: none;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
}

.process__node {
  position: absolute;
  inset-inline-start: calc(var(--space-lg) * -1 - 4px);
  top: 0.7rem;
  width: 10px;
  height: 10px;
  background: var(--surface-base);
  border: 2px solid var(--line-soft);
  border-radius: 50%;
  transition:
    border-color var(--dur-base) var(--ease-instrument),
    background-color var(--dur-base) var(--ease-instrument);
}

.process__stage[data-stage-state="active"] .process__node {
  background: var(--signal);
  border-color: var(--signal);
}

.process__name {
  font-family: var(--font-display);
  font-size: var(--fs-display-sub);
  font-weight: 400;
  letter-spacing: var(--track-sub);
}

.process__body {
  max-width: 56ch;
  color: var(--text-secondary);
}

.process__outcome {
  display: inline-block;
  color: var(--text-signal);
}

/* Progressive disclosure: only the active stage's body shows once
   JavaScript is running. Before data-js="on" lands (or with JS off) every
   stage's body is in the flow and reachable exactly as before. */
html[data-js="on"] .process__stage:not([data-stage-state="active"]) .process__body {
  display: none;
}

@media (min-width: 62rem) {
  .process {
    grid-template-columns: repeat(6, 1fr);
    gap: var(--grid-gap);
    padding-inline-start: 0;
    padding-block-start: var(--space-xl);
  }

  .process__track {
    inset: 0 0 auto 0;
    width: auto;
    height: 2px;
  }

  .process__fill {
    transform: scaleX(var(--fill));
    transform-origin: left;
  }

  .process__node {
    inset-inline-start: 0;
    top: calc(var(--space-xl) * -1 - 4px);
  }

  /* Six columns is narrow. The stage name drops to module scale so that
     "Commissioning" cannot run into the column beside it. Tracking stays
     at the corrected --track-sub value (0em, HIGH 3) rather than a
     one-off negative override. */
  .process__name {
    font-size: 1.5rem;
  }

  /* Outcome lines sit on a common baseline across all six stages. */
  .process__stage {
    grid-template-rows: auto 1fr auto;
  }
}

/* --- Industries in depth ---------------------------------------------------
   Rebuilt (Task C of the services-page redesign) as an index + stage: see
   the "24. Industries -- index + stage" fenced block further down this
   file. .industry-directory / .industry-list / .industry-block* /
   .industry-plate* and the M9 Plate Transition are gone -- the rejected
   carousel-plus-floating-plate layout they built is removed at the root,
   not patched. -------------------------------------------------------- */

/* --- Manufacturer names --------------------------------------------------- */
/* Now a .spec-grid (Sec 11.3); see the shared component under "Technical
   components" above. No dedicated CSS remains for the deleted
   .vendor-list / .vendor-note. */

/* --------------------------------------------------------------------------
   16. About sections
   -------------------------------------------------------------------------- */

.about-hero__grid {
  display: grid;
  gap: var(--space-xl);
  /* A clean two column header: the body column starts on the same line as the
     headline, rather than floating at the vertical centre. */
  align-items: start;
}

.about-hero__body {
  display: grid;
  gap: var(--space-xl);
}

.about-hero__figure {
  position: relative;
  overflow: hidden;
  aspect-ratio: 3 / 4;
  /* Asymmetric mask: the outer corners (everything but the lower-left,
     where the offset tile overlaps) carry the large plate radius; the
     overlapping inner corner takes the tighter vessel radius. */
  border-start-start-radius: var(--radius-plate);
  border-start-end-radius: var(--radius-plate);
  border-end-start-radius: var(--radius-vessel);
  border-end-end-radius: var(--radius-plate);
}

@media (min-width: 62rem) {
  .about-hero__grid {
    grid-template-columns: 7fr 5fr;
    gap: var(--space-2xl);
  }

  /* The figure sits to the right of the copy at desktop regardless of DOM
     order (body first in the markup, so mobile reads headline-then-image). */
  .about-hero__body {
    grid-column: 1;
    grid-row: 1;
  }

  .about-hero__media {
    grid-column: 2;
    grid-row: 1;
  }
}

/* --- Hero: second, smaller offset media tile (Sec 22) ---------------------
   Editorial overlap: about-account sits at the main image's lower-left
   corner, each figure's overlapping corner taking the tighter vessel
   radius so the two plates read as one deliberate joint rather than two
   unrelated cards. */
.about-hero__media {
  position: relative;
  /* Reserves room below the frame for the offset tile's overhang so it
     never collides with the section that follows. */
  margin-block-end: var(--space-2xl);
}

.about-hero__figure--offset {
  position: absolute;
  z-index: 2;
  /* Horizontal overhang is tied to --gutter (never more than 40px), not a
     percentage of the column -- a percentage of a wide auto/fr column can
     exceed the shell's own padding and overflow the viewport at widths
     narrower than --container-max, where the shell has no outside margin
     left to absorb it (found in QA at 768/1024). Gutter-bound overhang can
     never travel further than the shell's own padding box.
     Frame-only: no clip-path, no overflow, no border here -- this is the
     element initReveal()'s observer watches, and a collapsed [data-mask]
     clip-path on the observed element itself zeroes its own intersection
     rect and never fires (found in QA). The actual clipped, bordered box is
     the nested .media-mask below, which the observer never touches. */
  inset-block-end: -2rem;
  inset-inline-start: calc(var(--gutter) * -1);
  width: 58%;
  aspect-ratio: 4 / 5;
}

.about-hero__figure--offset .media-mask {
  border-start-start-radius: var(--radius-plate);
  border-start-end-radius: var(--radius-vessel);
  border-end-start-radius: var(--radius-plate);
  border-end-end-radius: var(--radius-plate);
  border: 4px solid var(--field-paper);
  box-shadow: var(--shadow-paper-lift);
}

@media (max-width: 61.999rem) {
  .about-hero__figure--offset {
    inset-block-end: -1.25rem;
    width: 52%;
  }
}

/* Text/photograph companion pairs used once each, on the two remaining
   About slots that need a frame of their own. */
.about-detail {
  display: grid;
  gap: var(--space-xl);
}

.about-detail__figure {
  position: relative;
  overflow: hidden;
  aspect-ratio: 1 / 1;
  max-width: 26rem;
  border-radius: var(--radius-vessel);
}

@media (min-width: 62rem) {
  .about-detail {
    grid-template-columns: 1fr auto;
    align-items: start;
    gap: var(--space-2xl);
  }
}

/* --- "Where the experience comes from": second offset tile (Sec 22) ------- */
.about-detail__media {
  position: relative;
  margin-block-end: var(--space-2xl);
}

@media (min-width: 62rem) {
  .about-detail__media {
    grid-column: 2;
    grid-row: 1;
  }
}

.about-detail__figure--offset {
  position: absolute;
  z-index: 2;
  /* Gutter-bound overhang -- see .about-hero__figure--offset above for why
     this isn't a percentage of the column. Frame-only for the same reason
     documented there: no clip-path/overflow/border on the observed element
     itself -- see the nested .media-mask instead. */
  inset-block-end: -1.5rem;
  inset-inline-end: calc(var(--gutter) * -1);
  width: 52%;
  aspect-ratio: 4 / 5;
}

.about-detail__figure--offset .media-mask {
  border-radius: var(--radius-vessel);
  border: 4px solid var(--field-paper);
  box-shadow: var(--shadow-paper-lift);
}

@media (max-width: 61.999rem) {
  .about-detail__figure--offset {
    inset-block-end: -1rem;
    width: 46%;
  }
}

.statement {
  max-width: var(--measure-tight);
  font-family: var(--font-display);
  font-size: var(--fs-display-sub);
  font-weight: 400;
  line-height: 1.3;
  letter-spacing: var(--track-sub);
}

.about-note {
  display: grid;
  gap: var(--space-2xs);
  padding: var(--space-md);
  background: var(--surface-sunken);
  border-inline-start: 2px solid var(--signal);
  border-radius: var(--radius-unit);
  font-size: var(--fs-body-sm);
  color: var(--text-secondary);
}

.pair-list {
  display: grid;
  border-block-start: var(--hairline) solid var(--line-soft);
}

.pair-list__row {
  display: grid;
  gap: var(--space-2xs);
  padding-block: var(--space-md);
  border-block-end: var(--hairline) solid var(--line-hair);
}

.pair-list__value {
  max-width: 60ch;
  color: var(--text-secondary);
}

@media (min-width: 48rem) {
  .pair-list__row {
    grid-template-columns: 14rem 1fr;
    gap: var(--space-lg);
    align-items: baseline;
  }
}

/* --- Small utilities ------------------------------------------------------ */

.measure-narrow {
  max-width: var(--measure-tight);
}

.mt-lg {
  margin-block-start: var(--space-lg);
}

.mt-xl {
  margin-block-start: var(--space-xl);
}

.mb-lg {
  margin-block-end: var(--space-lg);
}

/* --------------------------------------------------------------------------
   17. Plain PHP fallback page
   -------------------------------------------------------------------------- */

.page-plain {
  display: grid;
  place-items: center;
  min-height: 100svh;
  padding: var(--gutter);
  background: var(--field-ink);
}

.plain-shell {
  display: grid;
  gap: var(--space-md);
  max-width: 40rem;
}

.plain-lede {
  font-size: var(--fs-lead);
  color: var(--text-ink-secondary);
}

.plain-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
  max-width: none;
  margin-block-start: var(--space-sm);
}

.errors {
  display: grid;
  gap: var(--space-2xs);
  padding-inline-start: 1.25rem;
  list-style: disc;
  color: var(--state-error-dark);
}

/* --------------------------------------------------------------------------
   17a. Plate card -- shared card component (site-wide)
   DESIGN-BRIEF-v3.md Sec 2. One component wherever there is image + title +
   description + service/industry information; every page reuses it rather
   than a bespoke card class. Works unmodified on both [data-field="dark"]
   and [data-field="aqua"] ancestors via the field's own aliased tokens
   (--surface-raised / --line-hair); only the literal shadow pair below is
   field-specific because the spec calls exact values, not the existing
   --shadow-* tokens. No gradients: the --overlay scrim is a flat solid.
   -------------------------------------------------------------------------- */

.plate-card {
  position: relative;
  display: grid;
  align-content: start;
  border-radius: var(--radius-vessel);
  border: var(--hairline) solid var(--line-hair);
  background: var(--surface-raised);
  box-shadow: 0 1px 2px rgb(0 0 0 / 0.30);
  overflow: hidden;
  transition:
    transform var(--dur-3) var(--ease-arrive),
    border-color var(--dur-3) var(--ease-arrive),
    box-shadow var(--dur-3) var(--ease-arrive);
}

[data-field="aqua"] .plate-card {
  background: #ffffff;
  box-shadow: 0 1px 2px rgb(6 32 69 / 0.06);
}

/* The top-edge signal rule: drawn left-to-right on hover, never present at rest. */
.plate-card::after {
  content: "";
  position: absolute;
  inset-inline: 0;
  inset-block-start: 0;
  height: 2px;
  background: var(--brand-cyan);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform var(--dur-4) var(--ease-arrive);
}

@media (hover: hover) {
  .plate-card:hover {
    transform: translateY(-4px);
    border-color: var(--line-signal-dim);
    box-shadow: 0 18px 40px -20px rgb(0 0 0 / 0.65);
  }

  [data-field="aqua"] .plate-card:hover {
    box-shadow: 0 18px 40px -22px rgb(6 32 69 / 0.28);
  }

  .plate-card:hover::after {
    transform: scaleX(1);
  }

  .plate-card:hover .plate-card__media img {
    transform: scale(1.04);
  }
}

.plate-card__media {
  position: relative;
  overflow: hidden;
  border-radius: var(--radius-vessel) var(--radius-vessel) 0 0;
}

.plate-card__media img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform var(--dur-5) var(--ease-mass);
}

.plate-card__body {
  display: grid;
  gap: var(--space-2xs);
  padding: var(--space-md);
}

.plate-card__eyebrow {
  font-size: var(--fs-label-tech);
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-signal);
}

.plate-card__title {
  font-family: var(--font-display);
  font-size: var(--fs-title-module);
  font-weight: 400;
  line-height: var(--lh-module);
  color: var(--text-primary);
}

.plate-card__text {
  font-size: var(--fs-body-sm);
  line-height: var(--lh-body);
  color: var(--text-secondary);
}

/* Optional hairline list -- technical data, stays square. */
.plate-card__list {
  display: grid;
  margin-block-start: var(--space-2xs);
  border-block-start: var(--hairline) solid var(--line-hair);
  border-radius: var(--radius-flat);
}

.plate-card__list li {
  padding-block: var(--space-2xs);
  border-block-end: var(--hairline) solid var(--line-hair);
  font-size: var(--fs-body-sm);
  color: var(--text-secondary);
}

.plate-card__foot {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  margin-block-start: var(--space-2xs);
}

/* --overlay: body sits over the media on a flat scrim, no gradient. */
.plate-card--overlay .plate-card__media {
  position: absolute;
  inset: 0;
  border-radius: var(--radius-vessel);
}

.plate-card--overlay .plate-card__body {
  position: relative;
  z-index: 1;
  align-self: end;
  background: rgb(28 28 40 / 0.72);
  color: #ffffff;
}

.plate-card--overlay .plate-card__title,
.plate-card--overlay .plate-card__text,
.plate-card--overlay .plate-card__eyebrow {
  color: inherit;
}

/* --feature: dominant size, larger radius, larger type. */
.plate-card--feature {
  border-radius: var(--radius-plate);
}

.plate-card--feature .plate-card__media {
  border-radius: var(--radius-plate) var(--radius-plate) 0 0;
}

.plate-card--feature.plate-card--overlay .plate-card__media {
  border-radius: var(--radius-plate);
}

.plate-card--feature .plate-card__title {
  font-size: var(--fs-display-sub);
  line-height: var(--lh-sub);
}

/* --flush: no media, content only -- still a plate. */
.plate-card--flush .plate-card__body {
  padding: var(--space-lg);
}

/* Grid helper: asymmetric spans, never N identical rectangles. Mobile-first
   (single column); the --feature card only spans 2 columns at >=900px. */
.plate-grid {
  display: grid;
  gap: var(--space-lg);
}

@media (min-width: 900px) {
  .plate-grid--2 {
    grid-template-columns: repeat(2, 1fr);
  }

  .plate-grid--3 {
    grid-template-columns: repeat(3, 1fr);
  }

  .plate-grid .plate-card--feature {
    grid-column: span 2;
  }
}

/* --------------------------------------------------------------------------
   17b. Modal -- one shared component for Services solutions and technology
   DESIGN-BRIEF-v3.md Sec 8. CSS only here; site.js wires role="dialog" /
   focus trap / Escape / backdrop-click via the existing trapFocus(),
   inertSiblings(), lockScroll() and unlockScroll() helpers already proven
   by initInquiry() -- this mirrors .inquiry's visual language (same navy
   top rule, same overlay shadow, same close-button treatment) on purpose,
   so the two dialog systems read as one. Backdrop and radius are flat
   solids per Sec 0 -- no gradients.
   -------------------------------------------------------------------------- */

.modal {
  position: fixed;
  inset: 0;
  z-index: var(--z-overlay);
  display: none;
  align-items: center;
  justify-content: center;
  padding: var(--gutter);
}

.modal[data-state="open"] {
  display: flex;
}

.modal__scrim {
  position: absolute;
  inset: 0;
  width: 100%;
  border: 0;
  background: rgb(6 32 69 / 0.62);
  opacity: 0;
  cursor: default;
  transition: opacity var(--dur-4) var(--ease-arrive);
}

.modal[data-state="open"] .modal__scrim {
  opacity: 1;
}

.modal__panel {
  position: relative;
  display: flex;
  flex-direction: column;
  width: 100%;
  max-inline-size: min(56rem, calc(100vw - 2 * var(--gutter)));
  max-block-size: min(84vh, 52rem);
  background: var(--surface-base);
  color: var(--text-primary);
  border-block-start: 3px solid var(--brand-navy);
  border-radius: var(--radius-vessel);
  box-shadow: var(--shadow-overlay);
  opacity: 0;
  transform: scale(0.98);
  transition:
    opacity var(--dur-3) var(--ease-arrive),
    transform var(--dur-3) var(--ease-arrive);
}

.modal[data-state="open"] .modal__panel {
  opacity: 1;
  transform: none;
}

.modal__head {
  position: sticky;
  top: 0;
  z-index: 1;
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-sm);
  padding: var(--space-lg) var(--space-lg) var(--space-md);
  background: var(--surface-base);
  border-block-end: var(--hairline) solid var(--line-hair);
}

.modal__title {
  font-family: var(--font-display);
  font-size: var(--fs-display-sub);
  font-weight: 400;
}

.modal__close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 2.75rem;
  height: 2.75rem;
  border: var(--hairline) solid var(--line-soft);
  border-radius: var(--radius-control);
  color: var(--text-secondary);
  transition: color var(--dur-2) var(--ease-arrive), border-color var(--dur-2) var(--ease-arrive);
}

.modal__close:hover {
  color: var(--text-signal);
  border-color: var(--text-signal);
}

.modal__close svg {
  width: 1.125rem;
  height: 1.125rem;
}

.modal__body {
  padding: var(--space-lg);
  overflow-y: auto;
  overscroll-behavior: contain;
}

.modal__foot {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--space-sm);
  padding: var(--space-md) var(--space-lg) var(--space-lg);
  border-block-start: var(--hairline) solid var(--line-hair);
}

@media (max-width: 47.9375rem) {
  .modal {
    padding: 0;
  }

  .modal__panel {
    position: fixed;
    inset: auto 0 0 0;
    width: auto;
    max-inline-size: none;
    max-block-size: 92vh;
    border-radius: var(--radius-plate) var(--radius-plate) 0 0;
  }
}

/* --------------------------------------------------------------------------
   18. Reduced motion
   Per-motif fallbacks ship in addition to this blanket rule, because the
   fallbacks are what guarantee the end state is correct, not merely instant.
   -------------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }

  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }

  [data-reveal][data-reveal-state="out"] {
    opacity: 1;
    transform: none;
  }

  .trace-rule::after {
    transform: scaleX(1);
  }

  .trace-rule--v::after {
    transform: scaleY(1);
  }

  .node-marker[data-state="active"]::after {
    animation: none;
    display: none;
  }

  /* M4 reduced-motion fallback: the travelling segment never renders; the
     path stays the neutral --line-strong rule and the node is already in
     its active (filled) state via the M3 fallback above. */
  .signal-node__travel {
    display: none;
  }

  /* Hero progress: the hero falls back to a static poster and JS leaves
     data-active="false", so this is already invisible via the base rule
     above; the blanket transition-duration override just makes that snap
     rather than fade. No extra rule needed beyond the base opacity: 0. */
  .hero-progress {
    display: none;
  }

  /* M7 reduced-motion fallback: the active tab pill snaps to state, no
     colour transition. */
  .tech-tab {
    transition: none;
  }

  /* M9 reduced-motion fallback: instant swap, no mask, no 120ms offset. */
  .industry-plate__img {
    transition: none;
  }

  [data-mask] {
    clip-path: none;
  }

  /* Same fallback, matched to [data-mask][data-reveal-state="in"]'s own
     specificity (Sec 8) -- that selector is more specific than the bare
     [data-mask] rule above and would otherwise keep winning the cascade for
     any [data-mask] element whose data-reveal-state is already "in" (which
     is immediate for reduced-motion users: initReveal() sets it straight to
     "in" with no observer). Found in QA once about.html became the first
     page to actually pair data-mask with data-reveal on the same element. */
  [data-mask][data-reveal-state="in"] {
    clip-path: none;
  }

  .hero__scroll {
    height: auto;
  }

  .hero__stage {
    position: static;
    min-height: 88dvh;
  }

  .hero__copy {
    transform: none !important;
  }

  /* Task 2e: --copy/--settle are never written at all under reduced motion
     (initHero() returns before its rAF loop starts, site.js), so their
     defaults (1 / 0) already hold -- this just makes it explicit and
     guarantees no drift/tint/push-in transform is ever applied. */
  .hero__field {
    opacity: 1;
    transform: none;
    visibility: visible;
  }

  .hero__media {
    transform: none;
  }

  .mobile-nav {
    transform: none;
  }

  .mobile-nav__row {
    opacity: 1;
    transform: none;
  }

  /* Task 4 reduced-motion no-ops: .plate-card and .modal already get the
     blanket 0.01ms transition-duration above; these zero out the actual
     transform/opacity deltas so nothing visibly moves, only snaps. */
  .plate-card:hover {
    transform: none;
  }

  .plate-card:hover .plate-card__media img {
    transform: none;
  }

  .plate-card::after,
  .plate-card:hover::after {
    transition: none;
    transform: scaleX(1);
  }

  .modal__panel {
    transform: none !important;
  }
}

/* --------------------------------------------------------------------------
   19. Brand mark and the no-JavaScript inquiry fallback
   The mark is one flattened compound path, rendered with fill: currentColor,
   so it is coloured entirely through `color` on an ancestor -- see the
   [data-nav-over] rules in Section 10 and .brand--on-dark above. Permitted
   colours only: #1C1C28, #1EC1CB, #062045, #98DEEB.
   -------------------------------------------------------------------------- */

.mark__path {
  fill: currentColor;
}

.hero__title em {
  font-style: normal;
  font-weight: 400;
  color: rgb(255 255 255 / 0.82);
}

/* Without JavaScript the overlay is simply the last section of the page and
   the form posts natively. No dead button, no second page. */
html:not([data-js]) .inquiry {
  position: static;
  display: block;
}

html:not([data-js]) .inquiry__scrim,
html:not([data-js]) .inquiry__close {
  display: none;
}

html:not([data-js]) .inquiry__panel {
  width: 100%;
  max-width: var(--container-max);
  max-height: none;
  margin-inline: auto;
  overflow: visible;
  transform: none;
  box-shadow: none;
  /* data-field="panel" now lives on this element (see Sec 12 header), so
     its own field background (white ground) must stay -- do not force
     transparent here or the form falls back to dark-on-dark on the page
     ground. */
}

html:not([data-js]) .inquiry__head {
  position: static;
  background: none;
  padding-inline: 0;
}

html:not([data-js]) .inquiry__body {
  padding-inline: 0;
}

html:not([data-js]) .field-trap {
  display: none;
}

/* --------------------------------------------------------------------------
   20. No-JavaScript and hidden-element guarantees
   -------------------------------------------------------------------------- */

[hidden] {
  display: none !important;
}

/* With JavaScript the overlay is a drawer, so it starts closed and out of the
   accessibility tree. `data-js` is set by a one line inline script in the head
   so this applies on the first paint, with no flash of the form. */
html[data-js] .inquiry[data-state="closed"] {
  display: none;
}

/* Without JavaScript there is no drawer and no hamburger. The navigation and
   the inquiry form are both simply part of the page. */
html:not([data-js]) .nav {
  display: block;
  margin-inline-start: auto;
}

html:not([data-js]) .nav__list {
  flex-wrap: wrap;
  gap: var(--space-sm);
}

html:not([data-js]) .nav-toggle,
html:not([data-js]) .mobile-nav {
  display: none;
}

html:not([data-js]) .header__cta {
  display: inline-flex;
}

html:not([data-js]) .site-header__inner {
  flex-wrap: wrap;
  gap: var(--space-sm);
  padding-block: var(--space-sm);
}

/* The opening frame: displayed, but still in its closed transform. */
html[data-js] .inquiry[data-state="opening"] {
  display: flex;
}

.inquiry__success {
  display: grid;
  gap: var(--space-2xs);
  padding: var(--space-md);
  border: var(--hairline) solid rgb(31 107 69 / 0.35);
  border-radius: var(--radius-unit);
  background: rgb(31 107 69 / 0.06);
}

.inquiry__success h3 {
  font-size: var(--fs-display-sub);
}

/* Fourteen category tabs wrap to seven rows on a phone, which reads as
   clutter. Below the tablet breakpoint they become one scroll-snap strip. */
@media (max-width: 47.999rem) {
  .filter-bar {
    flex-wrap: nowrap;
    margin-inline: calc(var(--gutter) * -1);
    padding-inline: var(--gutter);
    padding-block-end: var(--space-2xs);
    overflow-x: auto;
    scroll-snap-type: x proximity;
    scroll-padding-inline: var(--gutter);
    overscroll-behavior-x: contain;
    scrollbar-width: thin;
  }

  .filter-tab {
    flex: none;
    scroll-snap-align: start;
  }
}


/* --------------------------------------------------------------------------
   21. Two column editorial prose
   Used once, on the About engineering philosophy block. The self-split
   (columns: 2) below is the <64rem fallback only: at >=64rem this prose sits
   in its own narrower column beside a sticky media column instead (Sec 25,
   .prose-media) so a second internal text split there would just cram two
   sets of columns side by side -- .prose-media overrides back to a single
   text column at that width, immediately below.
   -------------------------------------------------------------------------- */

/* Spacing goes on the trailing edge, not the leading one, so the paragraph
   that starts the second column is not pushed below the first column's top. */
.prose-cols p {
  max-width: var(--measure);
  margin-block-end: var(--space-md);
}

.prose-cols p:last-child {
  margin-block-end: 0;
}

@media (min-width: 48rem) {
  .prose-cols {
    columns: 2;
    column-gap: var(--space-2xl);
    orphans: 3;
    widows: 3;
  }

  .prose-cols p {
    max-width: none;
    break-inside: avoid;
  }
}

@media (min-width: 64rem) {
  .prose-media .prose-cols {
    columns: 1;
  }

  .prose-media .prose-cols p {
    max-width: none;
    break-inside: auto;
  }
}

/* ==========================================================================
   22. Services page hero -- asymmetric split (services.html only, Task A of
   the services-page redesign). Left column keeps the existing marker/h1/
   lead and adds a small index of anchor links into the page's own sections.
   Right column is a masked media panel that bleeds to the viewport edge.
   Entrance and parallax both reuse the site's existing primitives --
   [data-reveal]/[data-reveal-state] and the [data-plate] IntersectionObserver
   + rAF loop in site.js (data-plate-range lets this frame use a smaller
   +/-2% travel than the site's default +/-8%) -- no bespoke scroll listener.
   .page-hero itself (the shared base rule) is untouched; every override
   below is scoped to the new .page-hero--split modifier so about.html's
   [data-field="aqua"] hero is unaffected.
   ========================================================================== */

.page-hero__grid--split {
  display: grid;
  gap: var(--space-xl);
  max-width: none;
}

.page-hero__copy {
  display: grid;
  gap: var(--space-md);
  align-content: start;
}

/* Fix (post-review): a flex-wrap row of four items left "Industries" (the
   fourth) stranded alone on a second line at most widths -- the copy
   column is only 5fr of the 5fr/7fr split, rarely wide enough for all
   four navigation labels plus their counts on one row, and flex-wrap has
   no way to force a deliberate 2+2 break instead of wrapping wherever the
   last item happens to overflow. A fixed 2-column grid is width-
   independent: it is always exactly two rows of two, at every viewport,
   with no orphan possible. */
.page-hero__index {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  column-gap: var(--space-lg);
  margin-block-start: var(--space-xs);
  border-block-start: var(--hairline) solid var(--line-hair);
}

.page-hero__index-link {
  display: inline-flex;
  align-items: baseline;
  gap: 0.375rem;
  padding-block: var(--space-sm);
  border-block-end: 2px solid transparent;
  font-family: var(--font-text);
  font-size: var(--fs-body-sm);
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--text-secondary);
  transition: color var(--dur-2) var(--ease-arrive), border-color var(--dur-2) var(--ease-arrive);
}

.page-hero__index-link:hover,
.page-hero__index-link:focus-visible {
  color: var(--text-signal);
  border-color: var(--signal);
}

.page-hero__index-count {
  font-family: var(--font-text);
  font-size: 0.6875rem;
  font-weight: 600;
  color: var(--text-faint);
}

/* Fix (post-review): the decorative connector line between the copy and
   media columns never read as one deliberate stroke at any width -- with
   preserveAspectRatio="none" stretching a cross-column viewBox, it rendered
   as two near-flat disconnected segments and one of them cut straight
   through the lead paragraph's text. Per the "one clean idea beats a
   broken flourish" call, it is removed rather than patched; .page-hero__line
   is dead markup left in services.html with no CSS driving it (display:
   none is the default for an unstyled element, so nothing needs to render
   it here). */

/* Fix (post-review, reveal deadlock): .page-hero__media used to carry both
   data-reveal (so initReveal()'s IntersectionObserver watches it) AND the
   entrance clip-path collapsing it to a 0-height box at rest -- so the very
   first observer check saw a zero-size, non-intersecting target and could
   never fire, and the clip-path that would have opened it never received
   the "in" state that only fires on intersection. Self-locking. The fix:
   .page-hero__media itself now stays a plain, always-full-size, statically
   clipped (rounded overflow: hidden) box for the observer to measure
   correctly; the animated collapsing clip-path (and the counter-scale) both
   move onto .page-hero__media-scale, the inner wrapper, driven off the
   OUTER element's own data-reveal-state via a proper descendant selector
   (the earlier `[data-reveal-state="in"] .page-hero__media` rule was also
   wrong on its own terms: data-reveal-state is written onto .page-hero__media
   itself, never onto an ancestor, so a descendant-combinator selector could
   never have matched it even once the state did flip). */
.page-hero__media {
  position: relative;
  overflow: hidden;
  aspect-ratio: 4 / 3;
  margin-inline-end: calc(-1 * var(--gutter));
  border-radius: var(--radius-vessel) var(--radius-plate) var(--radius-plate) var(--radius-vessel);
  background: var(--surface-sunken);
}

.page-hero__media-scale {
  position: relative;
  display: block;
  width: 100%;
  height: 100%;
  clip-path: inset(0 0 100% 0 round var(--radius-vessel) var(--radius-plate) var(--radius-plate) var(--radius-vessel));
  transform: scale(1.06);
  transition: clip-path var(--dur-6) var(--ease-mass), transform var(--dur-6) var(--ease-mass);
}

.page-hero__media[data-reveal-state="in"] .page-hero__media-scale {
  clip-path: inset(0 round var(--radius-vessel) var(--radius-plate) var(--radius-plate) var(--radius-vessel));
  transform: scale(1);
}

/* Fix (post-review, round 2): a plain `height: 108%` on a normal-flow
   <img> only resolves through <picture> (not guaranteed block-level) some
   of the time -- it silently fell back to the image's own intrinsic 16:9
   height, leaving a dead band of plain background below the photo. The
   first attempt at fixing this (position: absolute with top/bottom insets
   and no explicit height) still didn't work, for a different, more
   specific reason: CSS 2.1 10.6.4 says an absolutely positioned REPLACED
   element with height: auto derives its used height from the intrinsic
   aspect ratio and the used width, ignoring top/bottom entirely -- <img>
   is a replaced element, so top/bottom-driven stretch (which works for a
   plain <div>) never applied here. Setting height to an explicit
   percentage (not auto) sidesteps that fallback: percentages on an
   absolutely positioned box resolve directly against the containing
   block's height regardless of intervening non-positioned elements
   like <picture>. */
.page-hero__media-img {
  position: absolute;
  top: -4%;
  left: 0;
  width: 100%;
  height: 108%;
  object-fit: cover;
  transform: translate3d(0, 0, 0);
}

@media (min-width: 64rem) {
  .page-hero--split {
    display: flex;
    align-items: center;
    min-height: clamp(520px, 72vh, 760px);
  }

  .page-hero--split .shell {
    width: 100%;
  }

  .page-hero__grid--split {
    grid-template-columns: 5fr 7fr;
    align-items: center;
    position: relative;
  }

  .page-hero__copy {
    grid-column: 1;
    grid-row: 1;
    padding-inline-end: var(--space-2xl);
    z-index: 1;
  }

  .page-hero__media {
    grid-column: 2;
    grid-row: 1;
    z-index: 1;
    /* Explicitly cancels the base (mobile) aspect-ratio: 4/3 rule. Paired
       with an explicit height, any aspect-ratio here would derive a fixed
       (height * ratio) width and disable the grid item's default stretch --
       which is exactly what the bleed-to-viewport-edge margin below depends
       on. Width comes from the grid column + that margin instead; height is
       independently clamped, so the panel reads as roughly 16:9 without
       being locked to it. */
    aspect-ratio: auto;
    height: clamp(400px, 56vh, 620px);
    /* Bleeds the panel's leading (right) edge to the viewport edge
       regardless of .shell's own centred max-width + gutter. When the
       viewport is still under --container-max, .shell spans 100vw, so the
       only gap to close is its own end padding (var(--gutter)); once the
       viewport exceeds --container-max, the letterboxed half also has to
       be crossed. max() keeps that second term at 0 in the first regime. */
    margin-inline-end: calc(
      -1 * (max(0px, (100vw - var(--container-max)) / 2) + var(--gutter))
    );
  }
}

/* --------------------------------------------------------------------------
   22a. Services hero -- reduced motion
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  .page-hero__media-scale {
    clip-path: inset(0 round var(--radius-vessel) var(--radius-plate) var(--radius-plate) var(--radius-vessel));
    transform: none;
  }
}

/* ==========================================================================
   23. Equipment section -- full-bleed category media (Task B of the
   services-page redesign). Each of the 14 .tech-panel articles becomes one
   full-bleed image with a genuinely opaque information panel layered over
   it (the panel's fill is --field-ink, the section's own solid dark ground,
   never a scrim or gradient). All 14 panels occupy the same CSS grid cell
   (html[data-js="on"] only -- see the no-JS note below) so the card's
   height never jumps between categories.
   ========================================================================== */

.tech-panels {
  display: grid;
}

html[data-js="on"] .tech-panel {
  grid-area: 1 / 1;
}

html[data-js="on"] .tech-panel[data-panel-state="inactive"] {
  display: none;
}

.tech-panel__plate {
  position: relative;
  display: block;
  /* Fix (post-review, round 2): 430-560px still wasn't tall enough for
     title + description + two-column six-term sample + button to fit
     without the panel's own internal scroll kicking in and hiding the
     button below the fold for the longer titles (e.g. "Flow measurement
     (gas, liquid, slurry and powder)" at eq-0). Raised the whole clamp;
     it is still one constant applied to all 14 categories alike, so the
     "no height jump between categories" requirement is unaffected. */
  height: clamp(500px, 69vh, 660px);
  border-radius: var(--radius-plate);
  overflow: hidden;
  background: var(--surface-sunken);
}

.tech-panel__media {
  position: absolute;
  inset: 0;
}

.tech-panel__media-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: clip-path var(--dur-5) var(--ease-mass), transform var(--dur-5) var(--ease-mass);
}

.tech-panel__info {
  position: absolute;
  inset-inline-start: var(--space-lg);
  inset-block-end: var(--space-lg);
  inset-inline-end: var(--space-lg);
  z-index: 1;
  display: grid;
  align-content: start;
  gap: var(--space-2xs);
  padding: var(--space-lg);
  background: var(--field-ink);
  border-radius: var(--radius-vessel);
  box-shadow: var(--rim-dark);
  transition: transform var(--dur-3) var(--ease-depart), opacity var(--dur-3) var(--ease-depart);
}

.tech-panel__title {
  font-size: var(--fs-display-sub);
}

/* Fix (post-review): scoped to this component only -- .spec-grid itself
   (repeat(auto-fill, minmax(11rem, 1fr))) is owned by another agent and is
   untouched. At the panel's own ~46%-of-card width, auto-fill resolved to
   one column, so six terms became six full-height rows; two fixed columns
   keep the sample's footprint predictable regardless of panel width. */
.tech-panel__sample {
  margin-block-start: var(--space-2xs);
  grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
}

/* Fix (post-review, round 2): below the tablet breakpoint the panel falls
   back to (100% - 2 * --space-lg) wide -- narrow enough that even two
   columns wrapped most terms to two lines and blew the card's height
   budget by 100-300px per category, well past what raising the card's own
   height clamp again could reasonably absorb without the card becoming a
   near-full-viewport slab. A horizontal scroll-snap strip (one row, each
   term its own width) is the fix: same six terms, same .spec-term cell
   styling untouched, just laid out as a row instead of a 2-column grid so
   the sample's own footprint stops depending on how long any one term's
   text is. */
@media (max-width: 47.999rem) {
  .tech-panel__sample {
    display: flex !important;
    flex-wrap: nowrap;
    gap: var(--hairline);
    overflow-x: auto;
    scroll-snap-type: x proximity;
    scrollbar-width: none;
    border-block-start: var(--hairline) solid var(--line-hair);
  }

  .tech-panel__sample::-webkit-scrollbar {
    display: none;
  }

  .tech-panel__sample .spec-term {
    flex: none;
    width: auto;
    max-width: 11rem;
    white-space: normal;
    scroll-snap-align: start;
  }
}

.tech-panel__rule {
  position: absolute;
  inset-block-start: 0;
  inset-inline-start: 0;
  width: 100%;
  height: 1px;
  background: var(--brand-cyan);
  transform: scaleX(1);
  transform-origin: left;
}

.tech-panel__title,
.tech-panel__desc,
.tech-panel__info .spec-grid,
.tech-panel__info .plate-card__foot {
  transition: transform var(--dur-4) var(--ease-arrive), opacity var(--dur-4) var(--ease-arrive);
}

.tech-panel__desc { transition-delay: 40ms; }
.tech-panel__info .spec-grid { transition-delay: 80ms; }
.tech-panel__info .plate-card__foot { transition-delay: 120ms; }

@media (min-width: 48rem) {
  .tech-panel__info {
    inset-inline-end: auto;
    /* Fix (post-review, round 2): widened from 46%/26rem -- the longest
       spec-term strings ("Concentration measurement, conductivity
       principle", "Temperature calibrators and temperature ovens") were
       wrapping to two lines inside the narrower two-column cell, which is
       what pushed eq-4/eq-8/eq-11's total content past the card's height
       even after the height clamp increase above. More width here means
       fewer of those terms wrap, which costs far less overall height than
       growing the card further would. */
    width: min(52%, 30rem);
  }
}

@media (min-width: 64rem) {
  .tech-panel__info {
    inset-inline-end: auto;
    width: auto;
    max-inline-size: 52%;
  }
}

/* --- Category change motion --------------------------------------------- */

/* Outgoing: image + panel translate ~6% in the travel direction and fade
   to 0 over --dur-3 with --ease-depart. */
.tech-panel[data-panel-state="leaving"][data-dir="1"] .tech-panel__media,
.tech-panel[data-panel-state="leaving"][data-dir="1"] .tech-panel__info {
  transform: translate3d(-6%, 0, 0);
  opacity: 0;
}

.tech-panel[data-panel-state="leaving"][data-dir="-1"] .tech-panel__media,
.tech-panel[data-panel-state="leaving"][data-dir="-1"] .tech-panel__info {
  transform: translate3d(6%, 0, 0);
  opacity: 0;
}

.tech-panel[data-panel-state="leaving"] .tech-panel__media {
  transition: transform var(--dur-3) var(--ease-depart), opacity var(--dur-3) var(--ease-depart);
}

/* Incoming image: pre-state jumps instantly (transition: none) to a
   directional clip-path mask + 8% counter-translate; adding .is-run then
   switches transition back on and animates to the resting (fully revealed,
   0-translate) values. Two-phase so the "jump" to the pre-state is never
   visible -- the same technique the existing M9 industry plate uses via a
   setTimeout offset, here done with an explicit reflow instead. */
.tech-panel[data-panel-state="entering"] .tech-panel__media-img {
  transition: none;
}

.tech-panel[data-panel-state="entering"][data-dir="1"] .tech-panel__media-img {
  clip-path: inset(0 0 0 100%);
  transform: translate3d(8%, 0, 0);
}

.tech-panel[data-panel-state="entering"][data-dir="-1"] .tech-panel__media-img {
  clip-path: inset(0 100% 0 0);
  transform: translate3d(-8%, 0, 0);
}

.tech-panel[data-panel-state="entering"].is-run .tech-panel__media-img {
  transition: clip-path var(--dur-5) var(--ease-mass), transform var(--dur-5) var(--ease-mass);
  clip-path: inset(0);
  transform: translate3d(0, 0, 0);
}

.tech-panel[data-panel-state="entering"] .tech-panel__title,
.tech-panel[data-panel-state="entering"] .tech-panel__desc,
.tech-panel[data-panel-state="entering"] .tech-panel__info .spec-grid,
.tech-panel[data-panel-state="entering"] .tech-panel__info .plate-card__foot {
  transition: none;
  transform: translate3d(0, 12px, 0);
  opacity: 0;
}

.tech-panel[data-panel-state="entering"].is-run .tech-panel__title,
.tech-panel[data-panel-state="entering"].is-run .tech-panel__desc,
.tech-panel[data-panel-state="entering"].is-run .tech-panel__info .spec-grid,
.tech-panel[data-panel-state="entering"].is-run .tech-panel__info .plate-card__foot {
  transition: transform var(--dur-4) var(--ease-arrive), opacity var(--dur-4) var(--ease-arrive);
  transform: none;
  opacity: 1;
}

.tech-panel[data-panel-state="entering"] .tech-panel__rule {
  transition: none;
  transform: scaleX(0);
}

.tech-panel[data-panel-state="entering"].is-run .tech-panel__rule {
  transition: transform var(--dur-6) var(--ease-draw) 80ms;
  transform: scaleX(1);
}

/* Resting states: identical end values to the ".is-run" targets above, so
   swapping data-panel-state from entering/leaving to active/inactive after
   the transition finishes never causes a visible jump. */
.tech-panel[data-panel-state="active"] .tech-panel__media,
.tech-panel[data-panel-state="active"] .tech-panel__info {
  transform: none;
  opacity: 1;
}

.tech-panel[data-panel-state="active"] .tech-panel__media-img {
  clip-path: inset(0);
  transform: translate3d(0, 0, 0);
}

.tech-panel[data-panel-state="active"] .tech-panel__title,
.tech-panel[data-panel-state="active"] .tech-panel__desc,
.tech-panel[data-panel-state="active"] .tech-panel__info .spec-grid,
.tech-panel[data-panel-state="active"] .tech-panel__info .plate-card__foot {
  transform: none;
  opacity: 1;
}

.tech-panel[data-panel-state="active"] .tech-panel__rule {
  transform: scaleX(1);
}

/* --- Progress indicator (replaces the "Category NN / 14" in-card eyebrow) */

.tech-progress {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  flex: 1;
  min-width: 0;
}

.tech-progress__track {
  display: flex;
  flex: 1;
  gap: 3px;
}

.tech-progress__segment {
  flex: 1;
  height: 2px;
  background: var(--line-strong);
  transition: background-color var(--dur-3) var(--ease-arrive);
}

.tech-progress__segment[data-state="active"] {
  background: var(--brand-cyan);
}

.tech-progress__count {
  flex: none;
  font-family: var(--font-text);
  font-size: var(--fs-body-sm);
  font-weight: 600;
  color: var(--text-secondary);
  white-space: nowrap;
}

/* Fix (post-review): 14 pills never fit one screen below the tablet
   breakpoint, and the row's own right edge gave no signal that more of it
   existed off-screen. .tech-directory only needs position: relative to
   host the hint badge -- it does not otherwise set position, so this is
   additive, not a change to its existing box model. */
.tech-directory {
  position: relative;
}

.tech-tablist__hint {
  display: none;
}

@media (max-width: 47.999rem) {
  .tech-tablist__hint {
    display: flex;
    align-items: center;
    justify-content: center;
    position: absolute;
    inset-block-start: 0;
    inset-inline-end: 0;
    width: 1.75rem;
    height: 1.75rem;
    /* A solid backing (the field's own colour, no gradient) plus a single
       inset hairline -- a static "there is more" mark, not a fade. */
    background: var(--field-ink);
    box-shadow: inset 1px 0 0 var(--line-strong);
    color: var(--text-faint);
    pointer-events: none;
  }

  .tech-tablist__hint svg {
    width: 0.875rem;
    height: 0.875rem;
  }
}

/* --------------------------------------------------------------------------
   23a. Equipment section -- reduced motion
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  .tech-panel__media,
  .tech-panel__media-img,
  .tech-panel__info,
  .tech-panel__title,
  .tech-panel__desc,
  .tech-panel__info .spec-grid,
  .tech-panel__info .plate-card__foot,
  .tech-panel__rule,
  .tech-progress__segment {
    transition: none !important;
  }

  .tech-panel[data-panel-state="entering"] .tech-panel__media-img,
  .tech-panel[data-panel-state="active"] .tech-panel__media-img {
    clip-path: inset(0);
    transform: none;
  }

  .tech-panel[data-panel-state="leaving"] .tech-panel__media,
  .tech-panel[data-panel-state="leaving"] .tech-panel__info {
    transform: none;
    opacity: 1;
  }

  .tech-panel[data-panel-state="entering"] .tech-panel__title,
  .tech-panel[data-panel-state="entering"] .tech-panel__desc,
  .tech-panel[data-panel-state="entering"] .tech-panel__info .spec-grid,
  .tech-panel[data-panel-state="entering"] .tech-panel__info .plate-card__foot {
    transform: none;
    opacity: 1;
  }

  .tech-panel[data-panel-state="entering"] .tech-panel__rule {
    transform: scaleX(1);
  }
}

/* ==========================================================================
   24. Industries -- index + stage (Task C of the services-page redesign,
   full replacement of the old carousel + floating plate). Left column is a
   vertical index of the six industry names as a real WAI-ARIA tablist;
   right column is a stage holding all six industry panels in the same grid
   cell. Same html[data-js="on"] gating as the equipment section above keeps
   every industry readable, in document order, with html:not([data-js="on"]).
   ========================================================================== */

.industry-stage-wrap {
  display: grid;
  gap: var(--space-lg);
}

.industry-index {
  display: flex;
  flex-wrap: nowrap;
  margin-inline: calc(var(--gutter) * -1);
  padding-inline: var(--gutter);
  overflow-x: auto;
  scroll-snap-type: x proximity;
  scroll-padding-inline: var(--gutter);
  scrollbar-width: none;
}

.industry-index::-webkit-scrollbar {
  display: none;
}

html:not([data-js="on"]) .industry-index {
  display: none;
}

.industry-index__row {
  flex: none;
  scroll-snap-align: start;
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  padding-block: var(--space-sm);
  padding-inline-end: var(--space-lg);
  border-inline-end: var(--hairline) solid var(--line-hair);
  margin-inline-end: var(--space-lg);
  background: none;
  border-block: none;
  border-inline-start: none;
  cursor: pointer;
  transform: translate3d(0, 0, 0);
  transition: transform var(--dur-3) var(--ease-arrive);
}

.industry-index__rule {
  width: 0;
  height: 1px;
  flex: none;
  background: var(--brand-cyan);
  transition: width var(--dur-4) var(--ease-draw);
}

.industry-index__name {
  font-family: var(--font-display);
  font-size: var(--fs-body);
  font-weight: 400;
  white-space: nowrap;
  color: var(--text-faint);
  transition: color var(--dur-3) var(--ease-arrive);
}

.industry-index__row:hover .industry-index__name,
.industry-index__row:focus-visible .industry-index__name {
  color: var(--text-secondary);
}

.industry-index__row[aria-selected="true"] .industry-index__name {
  color: var(--text-primary);
  font-weight: 600;
}

.industry-index__row[aria-selected="true"] .industry-index__rule {
  width: 24px;
}

.industry-stage {
  position: relative;
  display: grid;
}

html[data-js="on"] .industry-panel {
  grid-area: 1 / 1;
}

html[data-js="on"] .industry-panel[data-panel-state="inactive"] {
  display: none;
}

/* A fixed-height stage (like the equipment section's .tech-panel__plate)
   with the info panel layered over the image as an absolute overlay, not a
   separate block below it -- this is what keeps the stage height constant
   across all six industries even though their lead sentences are different
   lengths (an intrinsic block-below-image layout would resize with the
   text on every change, which the "no height jump" requirement rules out). */
.industry-panel {
  position: relative;
  height: clamp(420px, 54vh, 560px);
  border-radius: var(--radius-vessel) var(--radius-plate) var(--radius-plate) var(--radius-vessel);
  overflow: hidden;
  background: var(--surface-sunken);
}

.industry-panel__media {
  position: absolute;
  inset: 0;
  overflow: hidden;
}

.industry-panel__media-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: clip-path var(--dur-5) var(--ease-mass), transform var(--dur-5) var(--ease-mass);
}

.industry-panel__info {
  position: absolute;
  inset-inline-start: var(--space-lg);
  inset-block-end: var(--space-lg);
  inset-inline-end: var(--space-lg);
  display: grid;
  align-content: start;
  gap: var(--space-2xs);
  max-height: calc(100% - 2 * var(--space-lg));
  overflow-y: auto;
  padding: var(--space-lg);
  background: var(--field-ink);
  border-radius: var(--radius-vessel);
  box-shadow: var(--rim-dark);
  transition: transform var(--dur-3) var(--ease-depart), opacity var(--dur-3) var(--ease-depart);
}

.industry-panel__name {
  font-size: var(--fs-display-sub);
}

.industry-panel__lead,
.industry-panel__rest {
  color: var(--text-secondary);
}

html[data-js="on"] .industry-panel__rest[hidden] {
  display: none;
}

.industry-panel__more {
  justify-self: start;
  padding: 0;
  background: none;
  border: none;
  font: inherit;
  font-size: var(--fs-body-sm);
  font-weight: 600;
  color: var(--text-signal);
  cursor: pointer;
}

html:not([data-js="on"]) .industry-panel__more {
  display: none;
}

.industry-panel__name,
.industry-panel__lead,
.industry-panel__rest,
.industry-panel__more {
  transition: transform var(--dur-4) var(--ease-arrive), opacity var(--dur-4) var(--ease-arrive);
}

.industry-panel__lead { transition-delay: 40ms; }
.industry-panel__rest { transition-delay: 80ms; }
.industry-panel__more { transition-delay: 120ms; }

@media (min-width: 64rem) {
  .industry-stage-wrap {
    grid-template-columns: 36% 1fr;
    gap: var(--space-2xl);
    align-items: start;
  }

  .industry-index {
    display: grid;
    margin-inline: 0;
    padding-inline: 0;
    overflow-x: visible;
    border-block-start: var(--hairline) solid var(--line-hair);
  }

  .industry-index__row {
    padding-inline-end: 0;
    padding-block: var(--space-md);
    margin-inline-end: 0;
    border-inline-end: none;
    border-block-end: var(--hairline) solid var(--line-hair);
  }

  .industry-index__row[aria-selected="true"] {
    transform: translate3d(12px, 0, 0);
  }

  .industry-panel__info {
    inset-inline-end: auto;
    width: min(46%, 28rem);
  }
}

/* --- Industry change motion (vertical wipe; direction mirrors the index
   list -- moving to a later row wipes up from the bottom, an earlier row
   wipes down from the top) ------------------------------------------------ */

.industry-panel[data-panel-state="leaving"][data-dir="1"] .industry-panel__media,
.industry-panel[data-panel-state="leaving"][data-dir="1"] .industry-panel__info {
  transform: translate3d(0, -6%, 0);
  opacity: 0;
}

.industry-panel[data-panel-state="leaving"][data-dir="-1"] .industry-panel__media,
.industry-panel[data-panel-state="leaving"][data-dir="-1"] .industry-panel__info {
  transform: translate3d(0, 6%, 0);
  opacity: 0;
}

.industry-panel[data-panel-state="leaving"] .industry-panel__media,
.industry-panel[data-panel-state="leaving"] .industry-panel__info {
  transition: transform var(--dur-3) var(--ease-depart), opacity var(--dur-3) var(--ease-depart);
}

.industry-panel[data-panel-state="entering"] .industry-panel__media-img {
  transition: none;
}

.industry-panel[data-panel-state="entering"][data-dir="1"] .industry-panel__media-img {
  clip-path: inset(100% 0 0 0);
  transform: translate3d(0, 8%, 0);
}

.industry-panel[data-panel-state="entering"][data-dir="-1"] .industry-panel__media-img {
  clip-path: inset(0 0 100% 0);
  transform: translate3d(0, -8%, 0);
}

.industry-panel[data-panel-state="entering"].is-run .industry-panel__media-img {
  transition: clip-path var(--dur-5) var(--ease-mass), transform var(--dur-5) var(--ease-mass);
  clip-path: inset(0);
  transform: translate3d(0, 0, 0);
}

.industry-panel[data-panel-state="entering"] .industry-panel__name,
.industry-panel[data-panel-state="entering"] .industry-panel__lead,
.industry-panel[data-panel-state="entering"] .industry-panel__rest,
.industry-panel[data-panel-state="entering"] .industry-panel__more {
  transition: none;
  transform: translate3d(0, 12px, 0);
  opacity: 0;
}

.industry-panel[data-panel-state="entering"].is-run .industry-panel__name,
.industry-panel[data-panel-state="entering"].is-run .industry-panel__lead,
.industry-panel[data-panel-state="entering"].is-run .industry-panel__rest,
.industry-panel[data-panel-state="entering"].is-run .industry-panel__more {
  transition: transform var(--dur-4) var(--ease-arrive), opacity var(--dur-4) var(--ease-arrive);
  transform: none;
  opacity: 1;
}

.industry-panel[data-panel-state="active"] .industry-panel__media,
.industry-panel[data-panel-state="active"] .industry-panel__info {
  transform: none;
  opacity: 1;
}

.industry-panel[data-panel-state="active"] .industry-panel__media-img {
  clip-path: inset(0);
  transform: translate3d(0, 0, 0);
}

.industry-panel[data-panel-state="active"] .industry-panel__name,
.industry-panel[data-panel-state="active"] .industry-panel__lead,
.industry-panel[data-panel-state="active"] .industry-panel__rest,
.industry-panel[data-panel-state="active"] .industry-panel__more {
  transform: none;
  opacity: 1;
}

/* No-JS fallback: a plain readable stacked list of image + name + both
   paragraphs, in document order, index hidden (see above). .industry-panel
   keeps position: relative (it still needs it -- its media and info layers
   are absolutely positioned overlay children regardless of JS); dropping
   grid-area: 1/1 (gated to html[data-js="on"] above) is what's actually
   needed to make .industry-stage's base `display: grid` auto-place each
   panel into its own row instead of stacking them into one grid cell. */
html:not([data-js="on"]) .industry-stage {
  gap: var(--space-2xl);
}

@media (min-width: 64rem) {
  html:not([data-js="on"]) .industry-stage-wrap {
    grid-template-columns: 1fr;
  }
}

/* --------------------------------------------------------------------------
   24a. Industries -- reduced motion
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  .industry-index__row,
  .industry-index__rule,
  .industry-index__name,
  .industry-panel__media,
  .industry-panel__media-img,
  .industry-panel__info,
  .industry-panel__name,
  .industry-panel__lead,
  .industry-panel__rest,
  .industry-panel__more {
    transition: none !important;
  }

  .industry-panel[data-panel-state="entering"] .industry-panel__media-img,
  .industry-panel[data-panel-state="active"] .industry-panel__media-img {
    clip-path: inset(0);
    transform: none;
  }

  .industry-panel[data-panel-state="leaving"] .industry-panel__media,
  .industry-panel[data-panel-state="leaving"] .industry-panel__info,
  .industry-panel[data-panel-state="entering"] .industry-panel__name,
  .industry-panel[data-panel-state="entering"] .industry-panel__lead,
  .industry-panel[data-panel-state="entering"] .industry-panel__rest,
  .industry-panel[data-panel-state="entering"] .industry-panel__more {
    transform: none;
    opacity: 1;
  }
}

/* ============================================================================
   25. About page enrichment -- density pass (about.html only)
   New composition for the About page's hero index, editorial prose/image
   split, experience timeline, industries mosaic, method step grid and the
   long-term-support media panel. Every entrance/motion primitive here reuses
   the existing system: [data-reveal]/[data-reveal-state], [data-mask],
   [data-plate] (unchanged -- still exactly 3 frames on this page: the hero
   figure, the about-detail figure and the lap-plate) and .trace-rule /
   .trace-rule--v / .node-marker. The two scroll-linked fills below
   (prose-media parallax, pair-grid rail) are CSS scroll-driven animations
   (animation-timeline: view()), not scroll listeners, and are gated behind
   prefers-reduced-motion: no-preference so they degrade to a static,
   unanimated state automatically -- no separate reduced-motion overrides
   needed for them.
   ============================================================================ */

/* --- Shared "frame + mask" split for every masked media tile below --------
   [data-mask]'s own base rule (Sec 8) starts an element fully clipped
   (clip-path: inset(0 0 100% 0)) and opens it once its data-reveal-state
   reads "in". Pairing [data-mask] and [data-reveal] on the SAME element
   breaks that: initReveal()'s IntersectionObserver watches the element it
   is given, and a collapsed clip-path on that same element zeroes its own
   intersection rect -- the observer never reports it as intersecting, so
   data-reveal-state never reaches "in", so the clip-path never opens. A
   self-locking animation. Confirmed in QA against this page's four masked
   figures.
   Fix: every masked figure below is two elements. The outer .about-*
   figure carries [data-reveal] only -- plain position/sizing, never
   clipped, always safe for the observer to watch. The inner .media-mask
   carries [data-mask] (for the shared base clip-path/transition) and the
   figure's actual border-radius/border/overflow -- it is never itself
   observed, so its clip-path is free to start collapsed. This rule
   supplies the "open" state that [data-mask][data-reveal-state="in"] would
   normally have supplied, driven instead by the OUTER figure's own
   (safely-observed) reveal state -- additive only, does not touch Sec 8. */
.media-mask {
  display: block;
  position: relative;
  width: 100%;
  height: 100%;
  overflow: hidden;
}

/* picture only, not img -- every consumer's own *__img class already sets
   the image's width/height (several to a deliberate >100% for the M6-style
   oversize/drift treatment); a same-specificity `.media-mask img` rule here
   would fight those on height and win unpredictably by source order. */
.media-mask picture {
  display: block;
  width: 100%;
  height: 100%;
}

.about-hero__figure--offset[data-reveal-state="in"] .media-mask,
.about-detail__figure--offset[data-reveal-state="in"] .media-mask,
.prose-media__figure[data-reveal-state="in"] .media-mask,
.support-layout__figure[data-reveal-state="in"] .media-mask {
  clip-path: inset(0 0 0% 0);
}

/* --- Hero: hairline index of in-page anchors ------------------------------- */
.about-index {
  margin-block-start: var(--space-lg);
  max-width: 30rem;
  border-block-start: var(--hairline) solid var(--line-soft);
}

.about-index__list {
  display: grid;
  margin: 0;
  padding: 0;
  list-style: none;
}

.about-index__item {
  border-block-end: var(--hairline) solid var(--line-hair);
}

.about-index__link {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm);
  padding-block: var(--space-sm);
  font-family: var(--font-text);
  font-size: var(--fs-body-sm);
  font-weight: 500;
  color: var(--text-primary);
  text-decoration: none;
}

.about-index__arrow {
  display: inline-flex;
  flex: none;
  width: 0.875rem;
  height: 0.875rem;
  color: var(--text-signal);
  transform: translateX(0);
  transition: transform var(--dur-2) var(--ease-draw);
}

.about-index__link:hover .about-index__arrow,
.about-index__link:focus-visible .about-index__arrow {
  transform: translateX(4px);
}

/* --- "What engineering has to account for": prose + sticky media column --- */
.prose-media {
  position: relative;
}

.prose-media__rule {
  display: none;
}

/* Frame-only: no clip-path/overflow/radius on the element itself -- this is
   the [data-reveal] target initReveal() observes, and a collapsed
   [data-mask] clip-path on the observed element zeroes its own
   intersection rect and never fires (found in QA). The nested .media-mask
   carries the actual clip, radius and overflow instead. */
.prose-media__figure {
  position: relative;
  margin-block-start: var(--space-xl);
  aspect-ratio: 4 / 3;
}

.prose-media__figure .media-mask {
  border-start-start-radius: var(--radius-vessel);
  border-start-end-radius: var(--radius-plate);
  border-end-start-radius: var(--radius-plate);
  border-end-end-radius: var(--radius-plate);
}

.prose-media__img {
  width: 100%;
  height: 112%;
  object-fit: cover;
}

@media (min-width: 64rem) {
  .prose-media {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto minmax(0, 22rem);
    gap: var(--space-2xl);
    align-items: start;
  }

  .prose-media__rule {
    display: block;
    align-self: stretch;
  }

  .prose-media__figure {
    position: sticky;
    top: calc(var(--nav-h-lg) + var(--space-xl));
    margin-block-start: 0;
  }
}

/* A few percent of vertical drift as the pinned image's own view transits
   the viewport -- reinforces the sticky column's sense of depth against the
   scrolling prose beside it. Scroll-position-linked, not time-based, so a
   screenshot at any scroll offset is always a valid static frame; gated
   behind no-preference so reduced motion gets a perfectly still image. */
@media (min-width: 64rem) and (prefers-reduced-motion: no-preference) {
  .prose-media__img {
    animation: prose-media-drift linear both;
    animation-timeline: view();
    animation-range: cover;
  }
}

@keyframes prose-media-drift {
  from { transform: translateY(-3%); }
  to   { transform: translateY(3%); }
}

/* --- "Where the experience comes from": restrained timeline ---------------
   Three spans, labelled only with wording already in the copy above. */
.timeline {
  position: relative;
  margin-block-start: var(--space-md);
  padding-block-start: var(--space-lg);
}

.timeline__rule {
  position: absolute;
  inset-inline: 0;
  inset-block-start: calc(var(--space-lg) + 3px);
}

.timeline__list {
  position: relative;
  display: grid;
  gap: var(--space-lg);
  margin: 0;
  padding: 0;
  list-style: none;
}

.timeline__item {
  display: grid;
  gap: var(--space-2xs);
  align-content: start;
  justify-items: start;
}

.timeline__label {
  max-width: 26ch;
  font-size: var(--fs-body-sm);
  color: var(--text-secondary);
}

@media (min-width: 48rem) {
  .timeline__list {
    grid-template-columns: repeat(3, 1fr);
    gap: var(--space-md);
  }
}

/* --- Industries: asymmetric image mosaic, six tiles ------------------------
   2fr/1fr/1fr rhythm: a tall feature tile, two stacked pairs, one wide
   banner -- reuses .plate-card / .plate-card--overlay unmodified (Sec 17a),
   only the grid wrapper and tile sizing below are new.
   --industries-row / --industries-row-wide (used by the >=64rem grid below)
   are fluid per-row heights: the QA measurement pass found every tile
   showing only ~100px of actual photograph under the label panel (the
   banner tile worst of all, an 833px-tall source cropped to a 176px band),
   reading as a label list with image slivers rather than image-driven. */
.industries-mosaic {
  display: grid;
  gap: var(--grid-gap);
  margin-block-start: var(--space-sm);
  --industries-row: clamp(13rem, 9rem + 7.5vw, 16.5rem);
  --industries-row-wide: clamp(19rem, 13rem + 9vw, 24rem);
}

.industries-mosaic__tile {
  aspect-ratio: 3 / 2;
  /* .plate-card is normally an <article>; this is its first use as an <a>,
     which picks up the browser's default link underline on the title text
     with nothing in the shared component to remove it (found in QA --
     read as a stray rule under the label). Scoped to this container only,
     not a change to .plate-card itself. */
  text-decoration: none;
}

/* Solid label panel, not the shared component's own 0.72-alpha scrim --
   the brief requires >= 0.94 alpha here and six tiles need to read as one
   system regardless of how bright the individual photograph is (found
   marginal on the Sugar Cane / Chemical tiles in QA). --field-ink is the
   section's own ground colour, already fully opaque; scoped under this
   container so .plate-card--overlay's shared styling elsewhere (services.html)
   is untouched. */
.industries-mosaic .plate-card--overlay .plate-card__body {
  background: var(--field-ink);
}

/* Fix (found while raising the tile heights below): .plate-card__media
   img's own `height: 100%` (Sec 17a) silently fails to resolve here,
   because its parent <picture> has no definite height of its own --
   a percentage height only resolves against an ancestor with an explicit
   height, and <picture> (block via the global reset, Sec "img, picture,
   svg...") has none between the img and the already-sized
   .plate-card__media. Root cause matches the one already documented above
   .page-hero__media-img. Net effect: object-fit: cover was never actually
   cropping -- the img rendered at its own intrinsic aspect ratio and
   simply got clipped by .plate-card's overflow: hidden, so object-position
   had nothing to act on (worst case, the Sugar Cane tile: an 833px image
   silently overflowing a 176px box). Making the img itself position:
   absolute skips the non-positioned <picture> and resolves its inset
   directly against .plate-card__media (already sized via inset: 0, Sec
   17a) -- same technique as .page-hero__media-img. Scoped to this mosaic
   only, so .plate-card__media img elsewhere (services.html tech panels,
   solution cards) is untouched. */
.industries-mosaic .plate-card__media img {
  position: absolute;
  inset: 0;
}

/* Per-tile crop bias, chosen by eye against each source photograph so a
   taller box never cuts the working subject: metals and sugar cane carry
   their instrumentation low in frame, so those two are the only ones
   pulled noticeably off dead-centre. */
.industries-mosaic__tile--a .plate-card__media img { object-position: 50% 44%; }
.industries-mosaic__tile--b .plate-card__media img { object-position: 50% 42%; }
.industries-mosaic__tile--c .plate-card__media img { object-position: 50% 64%; }
.industries-mosaic__tile--d .plate-card__media img { object-position: 50% 40%; }
.industries-mosaic__tile--e .plate-card__media img { object-position: 50% 46%; }
.industries-mosaic__tile--f .plate-card__media img { object-position: 50% 62%; }

@media (min-width: 64rem) {
  .industries-mosaic {
    grid-template-columns: 2fr 1fr 1fr;
    grid-template-rows: var(--industries-row) var(--industries-row) var(--industries-row-wide);
    grid-template-areas:
      "a b c"
      "a d e"
      "f f f";
  }

  .industries-mosaic__tile {
    aspect-ratio: auto;
  }

  .industries-mosaic__tile--a { grid-area: a; }
  .industries-mosaic__tile--b { grid-area: b; }
  .industries-mosaic__tile--c { grid-area: c; }
  .industries-mosaic__tile--d { grid-area: d; }
  .industries-mosaic__tile--e { grid-area: e; }
  .industries-mosaic__tile--f { grid-area: f; }
}

/* The shared .link component's own ::after (Sec 7) is a fixed-width
   trailing dash sitting a full gap away from the text -- fine as a hover
   affordance next to short label text, but read as a disconnected stray
   rule here (found in QA) once the label text got longer ("Industries in
   detail"). content: none is the standard way to cancel one specific
   instance of an inherited pseudo-element; .link itself (Sec 7, shared
   site-wide) is untouched. Swapped for the same arrow glyph already used
   in the hero index above, for a consistent affordance. */
.industries-mosaic__more::after {
  content: none;
}

/* --- "How SIMSAN works": asymmetric 2x2 step grid + scroll-linked rail ---- */
.pair-grid {
  position: relative;
}

.pair-grid__rail {
  display: none;
}

.pair-grid__cells {
  display: grid;
  gap: var(--grid-gap);
}

.pair-plate {
  position: relative;
  display: grid;
  align-content: start;
  gap: var(--space-sm);
  padding: var(--space-lg);
  background: var(--surface-raised);
  border: var(--hairline) solid var(--line-hair);
  border-radius: var(--radius-vessel);
}

.pair-plate__rule {
  display: block;
  width: 2.5rem;
  height: var(--hairline);
  background: var(--line-strong);
}

.pair-plate--media {
  padding: 0;
  overflow: hidden;
}

.pair-plate--media .spec-term {
  margin-block-start: var(--space-md);
  padding-inline: var(--space-lg);
}

.pair-plate--media .pair-list__value {
  padding-inline: var(--space-lg);
  padding-block-end: var(--space-lg);
}

.pair-plate__figure {
  position: relative;
  aspect-ratio: 16 / 9;
  overflow: hidden;
}

.pair-plate__figure img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

@media (min-width: 48rem) {
  .pair-grid__cells {
    grid-template-columns: 3fr 2fr;
  }
}

@media (min-width: 64rem) {
  .pair-grid {
    display: grid;
    grid-template-columns: 1.5rem 1fr;
    gap: var(--space-xl);
    view-timeline-name: --pair-progress;
    view-timeline-axis: block;
  }

  .pair-grid__rail {
    display: block;
    position: relative;
    justify-self: center;
    width: var(--hairline);
    background: var(--line-hair);
  }

  .pair-grid__rail-fill {
    display: block;
    position: absolute;
    inset-inline: 0;
    inset-block-start: 0;
    width: 100%;
    height: 100%;
    background: var(--signal);
    transform-origin: top;
    transform: scaleY(0);
  }

  .pair-grid__cells {
    grid-column: 2;
  }
}

/* Fills as the reader moves through the step group -- scroll-position-linked
   via the named view-timeline on .pair-grid above, not a scroll listener.
   Gated to no-preference; under reduced motion the rail stays a plain
   unfilled hairline (no motion implied). */
@media (min-width: 64rem) and (prefers-reduced-motion: no-preference) {
  .pair-grid__rail-fill {
    animation: pair-rail-fill linear both;
    animation-timeline: --pair-progress;
    animation-range: cover;
  }
}

@keyframes pair-rail-fill {
  from { transform: scaleY(0); }
  to   { transform: scaleY(1); }
}

/* --- "Long term support": copy band above a full-width image band --------
   Two short paragraphs sit in a 12-col grid at desktop (lead / body side
   by side), the note runs full-measure below them, and the figure is a
   plain full-shell-width band underneath -- no overlap card, no fill
   pulled up over the photograph. */
.support-layout {
  display: grid;
  gap: var(--space-md);
}

.support-layout__head {
  margin-block-end: 0;
}

.support-layout__lead,
.support-layout__body {
  max-width: 34rem;
}

.support-layout__lead {
  font-size: var(--fs-lead);
}

.support-layout__figure {
  position: relative;
  aspect-ratio: 16 / 9;
  margin-block-start: var(--space-md);
}

.support-layout__figure .media-mask {
  border-radius: var(--radius-plate);
}

.support-layout__img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

@media (min-width: 64rem) {
  .support-layout {
    grid-template-columns: repeat(12, 1fr);
    column-gap: var(--grid-gap);
  }

  .support-layout__head {
    grid-column: 1 / span 12;
  }

  .support-layout__lead {
    grid-column: 1 / span 6;
  }

  .support-layout__body {
    grid-column: 7 / span 6;
  }

  .support-layout__note {
    grid-column: 1 / span 8;
  }

  .support-layout__figure {
    grid-column: 1 / span 12;
    aspect-ratio: 21 / 9;
  }
}

/* ==========================================================================
   25. Post-review spacing fixes -- #solutions and #manufacturers
   (services.html only). Scoped to these two section ids: --section-pad-y,
   .section-head, .seg-filter and .plate-card's own definitions are shared
   site-wide (and .plate-card sizing for #solutions is owned by another
   agent), so none of those base rules change here -- only this page's two
   sparse/oversized sections get a tighter vertical rhythm layered on top.
   ========================================================================== */

/* #solutions measured 952px at 1440 with 328px cards (untouched) -- the
   overspill was the section's own top/bottom padding plus the head/filter/
   carousel gaps stacking on top of each other. */
#solutions.section {
  padding-block: clamp(3rem, 1.6rem + 5.5vw, 6rem);
}

#solutions .section-head {
  margin-block-end: var(--space-lg);
}

#solutions .seg-filter {
  margin-block-end: 0;
}

#solutions .carousel.mt-lg {
  margin-block-start: var(--space-md);
}

/* #manufacturers measured 821px with a low ink ratio for its content (one
   heading, one lead line, a compact vendor grid, one footnote) -- same
   section-padding trim, plus tighter gaps between those four pieces. */
#manufacturers.section {
  padding-block: clamp(2.5rem, 1.4rem + 4.5vw, 5rem);
}

#manufacturers .section-head {
  margin-block-end: var(--space-md);
}

#manufacturers .mb-lg {
  margin-block-end: var(--space-md);
}

#manufacturers .mt-lg {
  margin-block-start: var(--space-md);
}
