/* ============================================================================
   main.css -- Trionova IT Consulting Services LLP, final static stylesheet.

   2026-08-28 migration (repo-wide policy reverted from Vite+Tailwind build to
   vanilla HTML/CSS/JS + GSAP, zero build pipeline): this project used
   Tailwind CSS v4 (@tailwindcss/vite) as its utility layer, with hand-
   authored CSS mixed in via the site's own @layer components block -- real
   Tailwind utility classes (hidden, lg:flex, grid, gap-12, md:grid-cols-4,
   hover:text-[...], etc.) appear directly in the generated HTML alongside
   this file's bespoke component classes (.nav, .btn-primary, .footer, ...).

   Since there is no bundler anymore to run Tailwind's JIT compiler at
   request time, the block below is Tailwind v4's ONE-TIME compiled output
   (npx @tailwindcss/cli@4.3.3, content scanned against every generated
   *.html file under src/, any depth) -- the exact same reset/theme/utility CSS
   @tailwindcss/vite used to generate at build time, just captured once as
   plain static CSS instead of regenerated on every request. It was produced
   from an isolated input (@import "tailwindcss" + this file's own @theme
   font-token block, nothing else) so the compiler never touched or
   reformatted the hand-authored CSS below -- that portion is byte-identical
   to what shipped under the old pipeline, comments included. Cross-checked
   class-for-class against the real npm run build output (dist/assets/
   main-*.css) before this file replaced it -- zero missing utility classes.

   Cascade-layer order is preserved exactly as Tailwind itself declares it
   (@layer properties; @layer theme, base, components, utilities; below) --
   this is what makes Tailwind utilities still correctly beat this file's
   own hand-authored @layer components rules on the same element, same as
   under the old build. sc-devices.css (the scrollcraft engine's device CSS,
   previously a separate file pulled in via @import "./sc-devices.css") is
   inlined verbatim into this same file below, in the same relative position
   it held in the old import order -- no more @import chain, one static file.

   Regenerate this compiled block (theme/base/utilities layers only, NEVER
   the hand-authored section) if Tailwind-style utility classes are ever
   added to new markup: run the @tailwindcss/cli package (pin the same
   version as below) against a temp entry file containing just
   `@import "tailwindcss";` plus this file's own @theme font-token block,
   with its content scanner pointed at every html file in the project, any
   depth, then paste the freshly compiled block back in above the
   "sc-devices.css" and hand-authored sections below, leaving them
   untouched. CAUTION, learned the hard way building this exact file:
   never write a literal two-asterisks-slash-asterisk glob token inside a
   CSS comment -- the "*" immediately before "/" closes the comment
   early and silently drops everything from @layer properties through
   @layer theme that follows it (confirmed live: cssRules showed @layer
   base as rule 0, @layer theme simply gone, --spacing/--font-display
   unresolved sitewide). Describe glob patterns in prose instead, as done
   just above.
   ============================================================================ */

/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
@layer properties;
@layer theme, base, components, utilities;
@layer theme {
  :root, :host {
    --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
      "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
      "Segoe UI Symbol", "Noto Color Emoji";
    --font-mono: "IBM Plex Mono", ui-monospace, monospace;
    --spacing: 0.25rem;
    --container-2xl: 42rem;
    --container-3xl: 48rem;
    --default-transition-duration: 150ms;
    --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
    --default-font-family: var(--font-sans);
    --default-mono-font-family: var(--font-mono);
    --font-display: "IBM Plex Sans", ui-sans-serif, system-ui, sans-serif;
  }
}
@layer base {
  *, ::after, ::before, ::backdrop, ::file-selector-button {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
    border: 0 solid;
  }
  html, :host {
    line-height: 1.5;
    -webkit-text-size-adjust: 100%;
    tab-size: 4;
    font-family: var(--default-font-family, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");
    font-feature-settings: var(--default-font-feature-settings, normal);
    font-variation-settings: var(--default-font-variation-settings, normal);
    -webkit-tap-highlight-color: transparent;
  }
  hr {
    height: 0;
    color: inherit;
    border-top-width: 1px;
  }
  abbr:where([title]) {
    -webkit-text-decoration: underline dotted;
    text-decoration: underline dotted;
  }
  h1, h2, h3, h4, h5, h6 {
    font-size: inherit;
    font-weight: inherit;
  }
  a {
    color: inherit;
    -webkit-text-decoration: inherit;
    text-decoration: inherit;
  }
  b, strong {
    font-weight: bolder;
  }
  code, kbd, samp, pre {
    font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);
    font-feature-settings: var(--default-mono-font-feature-settings, normal);
    font-variation-settings: var(--default-mono-font-variation-settings, normal);
    font-size: 1em;
  }
  small {
    font-size: 80%;
  }
  sub, sup {
    font-size: 75%;
    line-height: 0;
    position: relative;
    vertical-align: baseline;
  }
  sub {
    bottom: -0.25em;
  }
  sup {
    top: -0.5em;
  }
  table {
    text-indent: 0;
    border-color: inherit;
    border-collapse: collapse;
  }
  :-moz-focusring:where(:not(iframe)) {
    outline: auto;
  }
  progress {
    vertical-align: baseline;
  }
  summary {
    display: list-item;
  }
  ol, ul, menu {
    list-style: none;
  }
  img, svg, video, canvas, audio, iframe, embed, object {
    display: block;
    vertical-align: middle;
  }
  img, video {
    max-width: 100%;
    height: auto;
  }
  button, input, select, optgroup, textarea, ::file-selector-button {
    font: inherit;
    font-feature-settings: inherit;
    font-variation-settings: inherit;
    letter-spacing: inherit;
    color: inherit;
    border-radius: 0;
    background-color: transparent;
    opacity: 1;
  }
  :where(select:is([multiple], [size])) optgroup {
    font-weight: bolder;
  }
  :where(select:is([multiple], [size])) optgroup option {
    padding-inline-start: 20px;
  }
  ::file-selector-button {
    margin-inline-end: 4px;
  }
  ::placeholder {
    opacity: 1;
  }
  @supports (not (-webkit-appearance: -apple-pay-button))  or (contain-intrinsic-size: 1px) {
    ::placeholder {
      color: currentcolor;
      @supports (color: color-mix(in lab, red, red)) {
        color: color-mix(in oklab, currentcolor 50%, transparent);
      }
    }
  }
  textarea {
    resize: vertical;
  }
  ::-webkit-search-decoration {
    -webkit-appearance: none;
  }
  ::-webkit-date-and-time-value {
    min-height: 1lh;
    text-align: inherit;
  }
  ::-webkit-datetime-edit {
    display: inline-flex;
  }
  ::-webkit-datetime-edit-fields-wrapper {
    padding: 0;
  }
  ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field {
    padding-block: 0;
  }
  ::-webkit-calendar-picker-indicator {
    line-height: 1;
  }
  :-moz-ui-invalid {
    box-shadow: none;
  }
  button, input:where([type="button"], [type="reset"], [type="submit"]), ::file-selector-button {
    appearance: button;
  }
  ::-webkit-inner-spin-button, ::-webkit-outer-spin-button {
    height: auto;
  }
  [hidden]:where(:not([hidden="until-found"])) {
    display: none !important;
  }
}
@layer utilities {
  .invisible {
    visibility: hidden;
  }
  .visible {
    visibility: visible;
  }
  .absolute {
    position: absolute;
  }
  .fixed {
    position: fixed;
  }
  .relative {
    position: relative;
  }
  .static {
    position: static;
  }
  .sticky {
    position: sticky;
  }
  .isolate {
    isolation: isolate;
  }
  .container {
    width: 100%;
    @media (width >= 40rem) {
      max-width: 40rem;
    }
    @media (width >= 48rem) {
      max-width: 48rem;
    }
    @media (width >= 64rem) {
      max-width: 64rem;
    }
    @media (width >= 80rem) {
      max-width: 80rem;
    }
    @media (width >= 96rem) {
      max-width: 96rem;
    }
  }
  .mt-4 {
    margin-top: calc(var(--spacing) * 4);
  }
  .mt-6 {
    margin-top: calc(var(--spacing) * 6);
  }
  .mt-8 {
    margin-top: calc(var(--spacing) * 8);
  }
  .mt-10 {
    margin-top: calc(var(--spacing) * 10);
  }
  .mt-12 {
    margin-top: calc(var(--spacing) * 12);
  }
  .mt-auto {
    margin-top: auto;
  }
  .mb-1 {
    margin-bottom: var(--spacing);
  }
  .mb-2 {
    margin-bottom: calc(var(--spacing) * 2);
  }
  .mb-3 {
    margin-bottom: calc(var(--spacing) * 3);
  }
  .mb-4 {
    margin-bottom: calc(var(--spacing) * 4);
  }
  .mb-5 {
    margin-bottom: calc(var(--spacing) * 5);
  }
  .mb-6 {
    margin-bottom: calc(var(--spacing) * 6);
  }
  .mb-8 {
    margin-bottom: calc(var(--spacing) * 8);
  }
  .mb-10 {
    margin-bottom: calc(var(--spacing) * 10);
  }
  .mb-12 {
    margin-bottom: calc(var(--spacing) * 12);
  }
  .mb-14 {
    margin-bottom: calc(var(--spacing) * 14);
  }
  .mb-16 {
    margin-bottom: calc(var(--spacing) * 16);
  }
  .block {
    display: block;
  }
  .flex {
    display: flex;
  }
  .grid {
    display: grid;
  }
  .hidden {
    display: none;
  }
  .inline {
    display: inline;
  }
  .inline-block {
    display: inline-block;
  }
  .inline-flex {
    display: inline-flex;
  }
  .table {
    display: table;
  }
  .w-full {
    width: 100%;
  }
  .max-w-2xl {
    max-width: var(--container-2xl);
  }
  .max-w-3xl {
    max-width: var(--container-3xl);
  }
  .flex-1 {
    flex: 1;
  }
  .flex-none {
    flex: none;
  }
  .shrink {
    flex-shrink: 1;
  }
  .transform {
    transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);
  }
  .resize {
    resize: both;
  }
  .grid-cols-2 {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
  .flex-col {
    flex-direction: column;
  }
  .flex-wrap {
    flex-wrap: wrap;
  }
  .items-center {
    align-items: center;
  }
  .items-start {
    align-items: flex-start;
  }
  .justify-between {
    justify-content: space-between;
  }
  .justify-center {
    justify-content: center;
  }
  .gap-2 {
    gap: calc(var(--spacing) * 2);
  }
  .gap-3 {
    gap: calc(var(--spacing) * 3);
  }
  .gap-4 {
    gap: calc(var(--spacing) * 4);
  }
  .gap-5 {
    gap: calc(var(--spacing) * 5);
  }
  .gap-6 {
    gap: calc(var(--spacing) * 6);
  }
  .gap-7 {
    gap: calc(var(--spacing) * 7);
  }
  .gap-8 {
    gap: calc(var(--spacing) * 8);
  }
  .gap-12 {
    gap: calc(var(--spacing) * 12);
  }
  .gap-16 {
    gap: calc(var(--spacing) * 16);
  }
  .gap-px {
    gap: 1px;
  }
  .gap-x-12 {
    column-gap: calc(var(--spacing) * 12);
  }
  .gap-y-10 {
    row-gap: calc(var(--spacing) * 10);
  }
  .rounded {
    border-radius: 0.25rem;
  }
  .border {
    border-style: var(--tw-border-style);
    border-width: 1px;
  }
  .pt-4 {
    padding-top: calc(var(--spacing) * 4);
  }
  .pt-8 {
    padding-top: calc(var(--spacing) * 8);
  }
  .text-center {
    text-align: center;
  }
  .uppercase {
    text-transform: uppercase;
  }
  .tabular-nums {
    --tw-numeric-spacing: tabular-nums;
    font-variant-numeric: var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,);
  }
  .underline {
    text-decoration-line: underline;
  }
  .shadow {
    --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
    box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
  }
  .ring {
    --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
    box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
  }
  .outline {
    outline-style: var(--tw-outline-style);
    outline-width: 1px;
  }
  .blur {
    --tw-blur: blur(8px);
    filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
  }
  .drop-shadow {
    --tw-drop-shadow-size: drop-shadow(0 1px 2px var(--tw-drop-shadow-color, rgb(0 0 0 / 0.1))) drop-shadow(0 1px 1px var(--tw-drop-shadow-color, rgb(0 0 0 / 0.06)));
    --tw-drop-shadow: drop-shadow(0 1px 2px rgb(0 0 0 / 0.1)) drop-shadow( 0 1px 1px rgb(0 0 0 / 0.06));
    filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
  }
  .grayscale {
    --tw-grayscale: grayscale(100%);
    filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
  }
  .filter {
    filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
  }
  .backdrop-blur {
    --tw-backdrop-blur: blur(8px);
    -webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
    backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
  }
  .backdrop-filter {
    -webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
    backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
  }
  .transition {
    transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, content-visibility, overlay, pointer-events;
    transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
    transition-duration: var(--tw-duration, var(--default-transition-duration));
  }
  @media (hover: hover) {
    .hover\:text-\[var\(--accent-1\)\]:hover {
      color: var(--accent-1);
    }
  }
  @media (width >= 40rem) {
    .sm\:block {
      display: block;
    }
    .sm\:hidden {
      display: none;
    }
    .sm\:grid-cols-2 {
      grid-template-columns: repeat(2, minmax(0, 1fr));
    }
    .sm\:flex-row {
      flex-direction: row;
    }
    .sm\:whitespace-nowrap {
      white-space: nowrap;
    }
  }
  @media (width >= 48rem) {
    .md\:col-span-1 {
      grid-column: span 1 / span 1;
    }
    .md\:grid-cols-2 {
      grid-template-columns: repeat(2, minmax(0, 1fr));
    }
    .md\:grid-cols-3 {
      grid-template-columns: repeat(3, minmax(0, 1fr));
    }
    .md\:grid-cols-4 {
      grid-template-columns: repeat(4, minmax(0, 1fr));
    }
    .md\:flex-row {
      flex-direction: row;
    }
  }
  @media (width >= 64rem) {
    .lg\:block {
      display: block;
    }
    .lg\:flex {
      display: flex;
    }
    .lg\:hidden {
      display: none;
    }
    .lg\:inline-flex {
      display: inline-flex;
    }
    .lg\:grid-cols-2 {
      grid-template-columns: repeat(2, minmax(0, 1fr));
    }
    .lg\:grid-cols-3 {
      grid-template-columns: repeat(3, minmax(0, 1fr));
    }
    .lg\:grid-cols-5 {
      grid-template-columns: repeat(5, minmax(0, 1fr));
    }
    .lg\:grid-cols-\[1fr_1\.4fr\] {
      grid-template-columns: 1fr 1.4fr;
    }
    .lg\:grid-cols-\[58\%_38\%\] {
      grid-template-columns: 58% 38%;
    }
  }
}
@property --tw-rotate-x {
  syntax: "*";
  inherits: false;
}
@property --tw-rotate-y {
  syntax: "*";
  inherits: false;
}
@property --tw-rotate-z {
  syntax: "*";
  inherits: false;
}
@property --tw-skew-x {
  syntax: "*";
  inherits: false;
}
@property --tw-skew-y {
  syntax: "*";
  inherits: false;
}
@property --tw-border-style {
  syntax: "*";
  inherits: false;
  initial-value: solid;
}
@property --tw-ordinal {
  syntax: "*";
  inherits: false;
}
@property --tw-slashed-zero {
  syntax: "*";
  inherits: false;
}
@property --tw-numeric-figure {
  syntax: "*";
  inherits: false;
}
@property --tw-numeric-spacing {
  syntax: "*";
  inherits: false;
}
@property --tw-numeric-fraction {
  syntax: "*";
  inherits: false;
}
@property --tw-shadow {
  syntax: "*";
  inherits: false;
  initial-value: 0 0 #0000;
}
@property --tw-shadow-color {
  syntax: "*";
  inherits: false;
}
@property --tw-shadow-alpha {
  syntax: "<percentage>";
  inherits: false;
  initial-value: 100%;
}
@property --tw-inset-shadow {
  syntax: "*";
  inherits: false;
  initial-value: 0 0 #0000;
}
@property --tw-inset-shadow-color {
  syntax: "*";
  inherits: false;
}
@property --tw-inset-shadow-alpha {
  syntax: "<percentage>";
  inherits: false;
  initial-value: 100%;
}
@property --tw-ring-color {
  syntax: "*";
  inherits: false;
}
@property --tw-ring-shadow {
  syntax: "*";
  inherits: false;
  initial-value: 0 0 #0000;
}
@property --tw-inset-ring-color {
  syntax: "*";
  inherits: false;
}
@property --tw-inset-ring-shadow {
  syntax: "*";
  inherits: false;
  initial-value: 0 0 #0000;
}
@property --tw-ring-inset {
  syntax: "*";
  inherits: false;
}
@property --tw-ring-offset-width {
  syntax: "<length>";
  inherits: false;
  initial-value: 0px;
}
@property --tw-ring-offset-color {
  syntax: "*";
  inherits: false;
  initial-value: #fff;
}
@property --tw-ring-offset-shadow {
  syntax: "*";
  inherits: false;
  initial-value: 0 0 #0000;
}
@property --tw-outline-style {
  syntax: "*";
  inherits: false;
  initial-value: solid;
}
@property --tw-blur {
  syntax: "*";
  inherits: false;
}
@property --tw-brightness {
  syntax: "*";
  inherits: false;
}
@property --tw-contrast {
  syntax: "*";
  inherits: false;
}
@property --tw-grayscale {
  syntax: "*";
  inherits: false;
}
@property --tw-hue-rotate {
  syntax: "*";
  inherits: false;
}
@property --tw-invert {
  syntax: "*";
  inherits: false;
}
@property --tw-opacity {
  syntax: "*";
  inherits: false;
}
@property --tw-saturate {
  syntax: "*";
  inherits: false;
}
@property --tw-sepia {
  syntax: "*";
  inherits: false;
}
@property --tw-drop-shadow {
  syntax: "*";
  inherits: false;
}
@property --tw-drop-shadow-color {
  syntax: "*";
  inherits: false;
}
@property --tw-drop-shadow-alpha {
  syntax: "<percentage>";
  inherits: false;
  initial-value: 100%;
}
@property --tw-drop-shadow-size {
  syntax: "*";
  inherits: false;
}
@property --tw-backdrop-blur {
  syntax: "*";
  inherits: false;
}
@property --tw-backdrop-brightness {
  syntax: "*";
  inherits: false;
}
@property --tw-backdrop-contrast {
  syntax: "*";
  inherits: false;
}
@property --tw-backdrop-grayscale {
  syntax: "*";
  inherits: false;
}
@property --tw-backdrop-hue-rotate {
  syntax: "*";
  inherits: false;
}
@property --tw-backdrop-invert {
  syntax: "*";
  inherits: false;
}
@property --tw-backdrop-opacity {
  syntax: "*";
  inherits: false;
}
@property --tw-backdrop-saturate {
  syntax: "*";
  inherits: false;
}
@property --tw-backdrop-sepia {
  syntax: "*";
  inherits: false;
}
@layer properties {
  @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
    *, ::before, ::after, ::backdrop {
      --tw-rotate-x: initial;
      --tw-rotate-y: initial;
      --tw-rotate-z: initial;
      --tw-skew-x: initial;
      --tw-skew-y: initial;
      --tw-border-style: solid;
      --tw-ordinal: initial;
      --tw-slashed-zero: initial;
      --tw-numeric-figure: initial;
      --tw-numeric-spacing: initial;
      --tw-numeric-fraction: initial;
      --tw-shadow: 0 0 #0000;
      --tw-shadow-color: initial;
      --tw-shadow-alpha: 100%;
      --tw-inset-shadow: 0 0 #0000;
      --tw-inset-shadow-color: initial;
      --tw-inset-shadow-alpha: 100%;
      --tw-ring-color: initial;
      --tw-ring-shadow: 0 0 #0000;
      --tw-inset-ring-color: initial;
      --tw-inset-ring-shadow: 0 0 #0000;
      --tw-ring-inset: initial;
      --tw-ring-offset-width: 0px;
      --tw-ring-offset-color: #fff;
      --tw-ring-offset-shadow: 0 0 #0000;
      --tw-outline-style: solid;
      --tw-blur: initial;
      --tw-brightness: initial;
      --tw-contrast: initial;
      --tw-grayscale: initial;
      --tw-hue-rotate: initial;
      --tw-invert: initial;
      --tw-opacity: initial;
      --tw-saturate: initial;
      --tw-sepia: initial;
      --tw-drop-shadow: initial;
      --tw-drop-shadow-color: initial;
      --tw-drop-shadow-alpha: 100%;
      --tw-drop-shadow-size: initial;
      --tw-backdrop-blur: initial;
      --tw-backdrop-brightness: initial;
      --tw-backdrop-contrast: initial;
      --tw-backdrop-grayscale: initial;
      --tw-backdrop-hue-rotate: initial;
      --tw-backdrop-invert: initial;
      --tw-backdrop-opacity: initial;
      --tw-backdrop-saturate: initial;
      --tw-backdrop-sepia: initial;
    }
  }
}

/* ============================================================================
   Inlined from the former sc-devices.css (verbatim, unchanged) -- the
   scrollcraft engine's DEVICE mechanism CSS. See its own header comment
   immediately below for what it is and why it stays separate in spirit
   (own clearly-marked section) even though it now lives in one physical
   file post-migration.
   ============================================================================ */

/* ============================================================================
   sc-devices.css — the scrollcraft engine's DEVICE mechanism CSS only.
   Extracted from the skill's engine/scrollcraft.css (lines 174-432 there):
   the [data-sc-*] selectors the JS runtime actually drives. Deliberately
   NOT the skill file's tokens/reset/typography layers — this project already
   has its own equivalent (main.css's :root tokens + Tailwind preflight), and
   scrollcraft.css's un-layered `body`/`html` rules would otherwise beat this
   file's @layer components rules under CSS cascade-layer precedence
   regardless of import order. Do not restyle these selectors to taste; they
   are the mechanism scrollcraft.js reads/writes every frame. Style your own
   markup (the classes alongside data-sc-* attributes in home.mjs) instead.
   Token bridge: the --sc-* custom properties these rules read are aliased to
   this project's own DESIGN.md tokens directly below, not redefined. */

:root {
  --sc-canvas: var(--bg);
  --sc-surface: var(--bg-alt);
  --sc-ink: var(--ink);
  --sc-ink-soft: var(--muted);
  --sc-accent: var(--accent-1);
  --sc-accent-ink: var(--bg);
  --sc-hairline: var(--border-dark);
  --sc-gutter: clamp(1.25rem, 5vw, 5.5rem);
  --sc-maxw: 82rem;
  --sc-ease-out: cubic-bezier(0.23, 1, 0.32, 1);
  --sc-ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
  --sc-d-slow: 420ms;
  --sc-z-stage: 1; --sc-z-copy: 20; --sc-z-chrome: 60;
}

@layer components {
/* =============================================================== DEVICES ==
   Driven by the engine. Restyle your own markup, not these.                */

.sc-act--pinned { position: relative; }
.sc-stage {
  position: sticky; top: 0;
  height: 100vh; height: 100svh;
  overflow: clip;
  z-index: var(--sc-z-stage);
}

/* scrub / sequence media */
.sc-stage :is(video[data-sc-scrub], canvas[data-sc-sequence], .sc-stage__poster) {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
}
video[data-sc-scrub] { opacity: 0; transition: opacity 380ms var(--sc-ease-out); }
.sc-has-clip video[data-sc-scrub] { opacity: 1; }
/* The poster is a live frame-holder, not a placeholder: it stays up until a
   real video frame has painted, which is what stops the blank-stage flash. */
.sc-stage__poster { z-index: 0; transition: opacity 380ms var(--sc-ease-out); }
.sc-has-clip .sc-stage__poster { opacity: 0; }

/* A scrim only where text sits. A full-frame darkening layer flattens the
   image everywhere to fix contrast in one corner. */
.sc-scrim {
  position: absolute; inset: 0; pointer-events: none; z-index: 1;
  background: linear-gradient(
    var(--sc-scrim-angle, 180deg),
    color-mix(in oklab, var(--sc-canvas) var(--sc-scrim-a, 78%), transparent) 0%,
    color-mix(in oklab, var(--sc-canvas) 30%, transparent) 42%,
    transparent 68%);
}
/* Point the scrim at the copy. A scrim gradient that darkens the top while the
   copy sits at the bottom fixes contrast nowhere and flattens the image
   everywhere, which is the most common way text-over-video goes wrong. */
.sc-scrim--bottom { --sc-scrim-angle: 0deg; }
.sc-scrim--left   { --sc-scrim-angle: 90deg; }
.sc-scrim--right  { --sc-scrim-angle: 270deg; }

/* Corner scrims, for copy anchored to a corner. An edge gradient has to darken
   a whole band across the frame to cover one corner; a corner gradient puts the
   density where the text is and leaves the rest of the image alone. Pair with
   .sc-copy--lead / .sc-copy--trail. */
.sc-scrim--lead {
  background: linear-gradient(to top right,
    color-mix(in oklab, var(--sc-canvas) 94%, transparent) 0%,
    color-mix(in oklab, var(--sc-canvas) 72%, transparent) 30%,
    color-mix(in oklab, var(--sc-canvas) 30%, transparent) 52%,
    transparent 72%);
}
.sc-scrim--trail {
  background: linear-gradient(to top left,
    color-mix(in oklab, var(--sc-canvas) 94%, transparent) 0%,
    color-mix(in oklab, var(--sc-canvas) 72%, transparent) 30%,
    color-mix(in oklab, var(--sc-canvas) 30%, transparent) 52%,
    transparent 72%);
}
/* A band across the bottom, for copy that spans the full width of the frame.
   That is what both corner anchors become below 860px, and it is also the right
   shape whenever the copy block is wider than a corner. */
.sc-scrim--band {
  background: linear-gradient(to top,
    color-mix(in oklab, var(--sc-canvas) 94%, transparent) 0%,
    color-mix(in oklab, var(--sc-canvas) 74%, transparent) 22%,
    color-mix(in oklab, var(--sc-canvas) 30%, transparent) 42%,
    transparent 58%);
}
.sc-scrim--vignette {
  background: radial-gradient(120% 90% at 50% 45%, transparent 40%,
              color-mix(in oklab, var(--sc-canvas) 70%, transparent) 100%);
}

/* copy over a stage.
   max-width is in rem, never ch. A `ch` on this container resolves against the
   CONTAINER's font-size (body size), not the display size of the heading inside
   it, so `max-width: 20ch` here silently produces a ~180px column and wraps a
   hero headline to six lines. Set a ch measure on the text element itself. */
.sc-copy {
  position: absolute; z-index: var(--sc-z-copy);
  inset-inline: var(--sc-gutter);
  max-width: min(46rem, 76vw);
  will-change: opacity, transform;
}
.sc-copy--lead   { left: var(--sc-gutter); bottom: clamp(3rem, 12vh, 9rem); }
/* inset-inline FIRST. It is the shorthand for left+right, so declaring it after
   `left: 50%` resets left to auto and the block drifts off the left edge. */
.sc-copy--center { inset-inline: auto; left: 50%; top: 50%; translate: -50% -50%; text-align: center; }
.sc-copy--trail  { inset-inline: auto; right: var(--sc-gutter); bottom: clamp(3rem, 12vh, 9rem); text-align: right; }

/* cue defaults: the engine writes opacity/transform inline, these are the
   pre-paint state so nothing flashes before the first read() */
[data-sc-cue] { opacity: 0; will-change: opacity, transform; }
.sc-ready [data-sc-cue] { transition: none; }

/* kinetic split */
.sc-split { display: inline-block; overflow: hidden; vertical-align: top; }
/* Descenders live below the baseline; a line mask clipped to the line box eats
   the tails off g, y, p, j. The padding buys them room back. */
.sc-split--line { display: block; padding-bottom: 0.14em; margin-bottom: -0.14em; }
.sc-split__i { display: inline-block; will-change: transform, opacity; }
.sc-is-split { opacity: 1 !important; }

/* horizontal rail */
[data-sc-pan] { display: flex; will-change: transform; }

/* reveal + parallax */
[data-sc-reveal] { will-change: clip-path; }
[data-sc-parallax] { will-change: transform; }

/* flow reveal (fires once) */
[data-sc-in], [data-sc-stagger] > * {
  opacity: 0;
  transform: translate3d(0, 14px, 0);
  transition: opacity 620ms var(--sc-ease-out), transform 620ms var(--sc-ease-out);
}
[data-sc-in].sc-in, [data-sc-stagger] > .sc-in { opacity: 1; transform: none; }

/* pointer devices */
[data-sc-tilt] { transform-style: preserve-3d; will-change: transform; }
[data-sc-magnet] { will-change: transform; }
/* isolation always; position only if nothing more specific already set it.
   Written with :where() (zero specificity) on purpose: a plain `position:
   relative` here has the same weight as `.sc-stage` and, being later in the
   file, silently wins. A stage carrying both attributes then stops being
   sticky and the whole act scrolls away instead of pinning. */
[data-sc-spotlight] { isolation: isolate; }
:where([data-sc-spotlight]) { position: relative; }
[data-sc-spotlight]::after {
  content: ""; position: absolute; inset: 0; pointer-events: none; z-index: 2;
  background: radial-gradient(
    28rem 28rem at calc(var(--sc-mx, 0.5) * 100%) calc(var(--sc-my, 0.5) * 100%),
    color-mix(in oklab, var(--sc-accent) 16%, transparent), transparent 70%);
  opacity: 0; transition: opacity var(--sc-d-slow) var(--sc-ease-out);
}
[data-sc-spotlight]:hover::after { opacity: 1; }

/* =========================================================== WORLDFLIGHT ==
   One fixed stage for the whole page. The only thing in document flow is the
   spacer, and it is empty. If you find yourself adding a section here, you want
   act mode instead: the moment a real block scrolls past the fixed stage, the
   page has a seam again and the whole point is gone.                        */

[data-sc-mode="worldflight"] { position: relative; }

.sc-world {
  position: fixed; inset: 0;
  overflow: clip;
  z-index: var(--sc-z-stage);
  background: var(--sc-canvas);
}

/* Every leg is mounted for the life of the page and stacked in the same box.
   The engine owns opacity, visibility and z-index here; do not set them. */
.sc-world__seg {
  position: absolute; inset: 0;
  opacity: 0;
  will-change: opacity;
}
.sc-world__seg > :is(video, img, canvas) {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
}
/* The poster is the frame-holder AND the first camera move: the engine pushes
   it in slowly until a real decoded frame is available to replace it. */
.sc-world__poster {
  z-index: 0;
  transform-origin: 50% 50%;
  transition: opacity 420ms var(--sc-ease-out);
}
.sc-world__seg > video { z-index: 1; }
.sc-world__seg.sc-has-clip .sc-world__poster { opacity: 0; }
/* The act stylesheet lights a clip through its ancestor act. A worldflight leg
   has no act, so match the class the engine also writes on the clip itself. */
video[data-sc-scrub].sc-has-clip { opacity: 1; }

/* The copy layer is fixed too, and inert by default: the engine hands
   pointer-events back to a block only while it is actually legible. */
.sc-world__copy {
  position: fixed; inset: 0;
  z-index: var(--sc-z-copy);
  pointer-events: none;
}
.sc-world__scrim { position: absolute; inset: 0; pointer-events: none; z-index: 0; }
[data-sc-copy] {
  opacity: 0;
  pointer-events: none;
  will-change: opacity, transform;
}
.sc-ready [data-sc-copy] { transition: none; }

/* The track. Empty on purpose. */
.sc-world__spacer { pointer-events: none; }

/* scroll progress */
[data-sc-progress] {
  position: fixed; inset: 0 0 auto 0; height: 2px; z-index: var(--sc-z-chrome);
  background: var(--sc-accent); transform-origin: 0 50%; transform: scaleX(0);
  will-change: transform;
}

/* ----------------------------------------------------------- atmosphere --
   Depth is not only shadow. Grain keeps a flat dark ground from banding, and
   it is the difference between "a dark page" and "a lit room". */
.sc-grain { position: fixed; inset: 0; pointer-events: none; z-index: 3; opacity: 0.045; }
.sc-grain::before {
  content: ""; position: absolute; inset: -50%;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='140' height='140'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3'/></filter><rect width='140' height='140' filter='url(%23n)'/></svg>");
}

/* -------------------------------------------------------- reduced motion -- */
@media (prefers-reduced-motion: reduce) {
  /* Fewer and gentler, not zero. Opacity still carries the reveal so the page
     is comprehensible; every position change is dropped. */
  [data-sc-in], [data-sc-stagger] > * { transform: none; transition-duration: 220ms; }
  [data-sc-parallax], [data-sc-pan], .sc-split__i { transform: none !important; }
  /* A wipe is a position change too. Show reveal content settled. */
  [data-sc-reveal] { clip-path: none !important; }
  /* A rail's transform IS its navigation, not decoration. Zeroing it the way
     parallax and kinetic type are zeroed parks the act on its first screenful
     and makes everything past the fold unreachable: a device degrading into
     missing content. Hand the travel back to the reader as an ordinary scroll
     region instead, so the same items are all still gettable without motion.
     A build that would rather re-lay the rail out as a grid can still do that;
     this is the floor, not the ceiling. */
  [data-sc-act="pan"] .sc-stage,
  [data-sc-act="pan"] [data-sc-stage] {
    overflow-x: auto; overflow-y: hidden;
    overscroll-behavior-x: contain;
    scroll-snap-type: x proximity;
  }
  [data-sc-pan] > * { scroll-snap-align: center; }
  [data-sc-spotlight]::after { display: none; }
  /* A worldflight keeps its whole story here. No clip is ever fetched, so the
     posters are the film, and they cross-dissolve through exactly the same
     seams at exactly the same scroll positions. The push-in and the copy drift
     are the only things that go. */
  .sc-world__poster, [data-sc-copy] { transform: none !important; }
}

/* --------------------------------------------------------------- mobile -- */
@media (max-width: 860px) {
  .sc-copy { inset-inline: var(--sc-gutter); max-width: none; }
  .sc-copy--trail { text-align: left; }
  .sc-stage { height: 100svh; }
  /* .sc-copy--trail re-anchors to the left and spans the full width here, so a
     bottom-RIGHT corner gradient darkens the one corner the copy just left and
     guarantees a contrast failure over any bright clip. Point the density at
     the copy: the same band .sc-scrim--band paints. */
  .sc-scrim--trail {
    background: linear-gradient(to top,
      color-mix(in oklab, var(--sc-canvas) 94%, transparent) 0%,
      color-mix(in oklab, var(--sc-canvas) 74%, transparent) 22%,
      color-mix(in oklab, var(--sc-canvas) 30%, transparent) 42%,
      transparent 58%);
  }
}
}

/* ============================================================================
   Hand-authored site CSS resumes below (originally main.css from this same
   line onward -- the @import/@theme lines above it are gone, replaced by
   the compiled block at the top of this file; everything from here down is
   untouched).
   ============================================================================ */

/* ===== Theme tokens (2026-08-14, client-directed: "include the light and
   dark theme toggle") =====
   Dark was previously the only theme — `--bg-light`/`--ink-light`/etc.
   existed only as a LOCAL per-section pair (the Why Trionova problem card,
   the kanban cards' text-on-color needs), never as a real site-wide
   alternative to the root tokens. Making the toggle genuine — not just a
   button that does nothing — means the CORE tokens themselves (--bg, --ink,
   --muted, --border-dark, --surface) have to actually flip value under
   `[data-theme="light"]`, not just get a same-named sibling nobody reads by
   default. Reuses the *-light values that already existed for exactly this
   purpose rather than inventing new colors.
   Also introduces --fill-1..4: generic neutral overlay tokens (subtle card
   fills, hover states, border-gradient highlights). These previously lived
   as scattered rgba(255,255,255,X) literals throughout this file — every
   one of those silently assumes a dark backdrop; on a light background the
   same white-alpha value reads as barely-there or outright wrong-direction
   contrast. Component rules below are being migrated from the literals to
   these tokens as each one is touched; see individual rules for progress. */
/* ---------------------------------------------------------------------------
   2026-08-24 (round 2) — token polarity inverted to the house pattern.
   Was: `:root` = dark, `[data-theme="light"]` + `@media (prefers-color-scheme:
   light)` = light, and the inline <head> script hardcoded 'light' for every
   first-time visitor regardless of their OS setting. Now: bare `:root` is the
   LIGHT default (so a page with CSS but no JS, and any element that escapes
   the attribute selector, lands somewhere legible), `@media (prefers-color-
   scheme: dark)` guarded by `:not([data-theme="light"])` carries the dark
   palette for visitors whose OS asks for it, and `[data-theme="dark"]` /
   `[data-theme="light"]` win over both so an explicit toggle always beats the
   OS. Every token below has a value in BOTH branches — including the
   precision-engineering "world" tokens (--hero-*, --grid-*, --signal-*),
   which were previously unconditional-dark and put a pale glass nav on top of
   a black hero for every default visitor. See build-log.md 2026-08-24 #2.
   --------------------------------------------------------------------------- */
:root {
  /* ---- theme-invariant ---- */
  --surface-light: #FFFFFF; /* the deliberate "light card on a dark band" island — e.g. the problem card — independent of overall site theme */
  --bg-light: #F2F5F3; /* .section.bg-light — a light BAND within an otherwise-dark page, independent of the site-wide theme toggle, same as --surface-light above */
  --ink-light: #10141A;
  --muted-light: rgba(16,20,26,0.68);
  --border-light: rgba(14,20,26,0.08);

  /* ---- LIGHT palette (the default) ---- */
  --bg: #F6F8FA;
  --bg-alt: #ECF0F3;
  --surface: linear-gradient(180deg, rgba(10,15,25,0.03), rgba(10,15,25,0.008));
  --ink: #10141A;
  --ink-rgb: 16,20,26;
  --muted: rgba(16,20,26,0.68);
  --border-dark: rgba(14,20,26,0.09);
  --fill-1: rgba(10,15,25,0.035);
  --fill-2: rgba(10,15,25,0.05);
  --fill-3: rgba(10,15,25,0.08);
  --fill-4: rgba(10,15,25,0.14);
  --accent-glow: rgba(62,125,245,0.22);
  --glass-bg: rgba(255,255,255,0.55);
  --glass-bg-scrolled: rgba(255,255,255,0.85);
  --glass-bg-panel: #FFFFFF;
  --glass-bg-fallback: rgba(255,255,255,0.94);
  --glass-border: rgba(14,20,26,0.1);

  /* ---- the precision-engineering "world" (hero, incident, silence beat,
     ops readout, every schematic panel sitewide). In dark this is a control
     room; in light it is the same instrument read off a blueprint sheet —
     cool paper, ink-blue rules, the same signal semantics at darker values
     so they still clear 4.5:1 against paper. NOT a mechanical inversion:
     the grid is denser and cooler in light because a hairline that reads as
     "glow" on black reads as "smudge" on white. ---- */
  --hero-canvas: #EDF1F6;
  --hero-canvas-rgb: 237,241,246;
  --hero-canvas-deep: #E2E8F0; /* the darker end of the world's own gradient */
  --hero-ink: #080D14;
  --hero-ink-rgb: 8,13,20;
  /* The ground the incident copy sits on, INSIDE the world. Deliberately not
     --hero-canvas: a panel painted in the world's own colour over the world's
     own lattice is not a panel, it is a slightly blurrier patch of lattice.
     In light theme it steps toward white so the card reads as an object; in
     dark it steps toward black for the same reason. */
  --incident-card-rgb: 255,255,255;
  --grid-line: rgba(27,69,196,0.10);
  --grid-line-strong: rgba(27,69,196,0.20);
  --hero-scan: rgba(10,20,40,0.030);
  --hero-vignette: rgba(120,140,175,0.22);
  --signal-ok: #077A5F; /* darkened from #0A8A6C — 4.05:1 -> 4.6:1 on --bg in light */
  --signal-ok-rgb: 7,122,95;
  --signal-alert: #C42A1D;
  --signal-alert-rgb: 196,42,29;
  --signal-amber: #8F5A05;
  --signal-amber-rgb: 143,90,5;
  /* Blue accent (2026-08-13, client-directed) — replaces the earlier teal
     brand system so the site matches logo.jpeg's blue/chrome TNT mark
     instead of clashing with it (flagged as an open conflict when the real
     logo was first integrated, left unresolved until the client confirmed
     "switch the site accent to logo blue" over the dual-accent/nav-only
     alternatives). --accent-1 is tuned bright enough for 4.5:1+ text/icon
     contrast against --bg; --accent-2 is a deeper blue tuned for white
     button text and gradient partner duty — the two colors are deliberately
     NOT the same lightness the way the old teal pair was, because a single
     hue can't clear both "readable as text on dark bg" and "readable as
     white-on-button-bg" at once. See build-log.md for the full contrast-math
     rationale and the AI-generated imagery (hub-core/icon-database/
     icon-application/icon-cloud) that was regenerated with a blue prompt to
     match — icon-shield/icon-license stay amber deliberately, unrelated
     accent used for the risk/licensing domains, not the palette conflict. */
  --accent-1: #3E7DF5;
  --accent-1-rgb: 62,125,245;
  --accent-2: #1B45C4;
  --accent-on-light: #1B45C4;
  --accent-on-light-text: #12307F; /* darkened for 4.5:1+ AA at small sizes on light sections */
  --nav-h: 70px;
  /* 2026-08-26 — nav is now a flush, full-width bar (see .nav), not an
     inset floating pill, so there's no gap above it to account for. Left at
     0 rather than removed: every scroll-offset calc below still adds
     --nav-h + --nav-top + its own buffer, and 0 keeps those correct without
     hunting down each one. */
  --nav-top: 0px;
}

/* ---------------------------------------------------------------------------
   2026-08-26 — dark edition retired (client-directed: "remove dark and
   light edition option and use just 1 edition"). The toggle, its inline
   <head> script, and the theme-toggle.js module are gone; `data-theme` is
   now hardcoded to "light" by that inline script and never changes at
   runtime, so the `@media (prefers-color-scheme: dark)` and
   `:root[data-theme="dark"]` palette blocks that used to live here are
   unreachable and have been removed rather than left as dead weight. The
   site's one edition is the LIGHT palette declared on bare `:root` above,
   accented per-section the way techorbitgroup.com does (amber for
   licensing/continuity, blue elsewhere) rather than via a second global
   theme. `[data-theme="light"]` selectors elsewhere in this file still
   apply — the attribute is always present with that value — so they were
   left alone rather than mass-edited to bare selectors.
   --------------------------------------------------------------------------- */

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

html { scroll-padding-top: calc(var(--nav-h) + var(--nav-top) + 16px); overflow-x: clip; scroll-behavior: smooth; }

body {
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font-display);
  font-size: 17px;
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
  overflow-x: clip; /* clip, not hidden — doesn't create a new scroll container or swallow focus-outline overflow */
  /* Motion QA finding (2026-08-14): the theme toggle flipped data-theme with
     zero crossfade — body's background/text hard-snapped while .nav right
     above it faded its own background over 300ms (main.css .nav rule below),
     two adjacent elements changing color at different speeds on the same
     click. transition-property is explicit (not `all`) so this never fights
     layout/transform transitions elsewhere. */
  transition: background-color 0.3s ease, color 0.3s ease;
}

/* Tailwind's preflight resets every heading to font-size/font-weight:
   inherit — h1/h2/h3 must get an explicit size here or they silently
   collapse to body text size. (They did, site-wide, until this was caught
   by the impeccable critique pass and verified via computed style: h1 and
   h2 were both rendering at 17px, same as body copy.) */
/* Type scale (2026-08-14, "reimagine the design" — Nexdeep3's own tokens
   run .hero-h1 equivalent at clamp(4rem,7.2vw,9rem)/64–144px against
   short 2-line headlines; Trionova's copy runs longer (multi-line
   sentence-length headlines, not 2-word taglines), so pushed meaningfully
   bigger than the old clamp(44px,7vw,96px) without matching Nexdeep's raw
   number 1:1 — a genuinely huge scale on long copy just wraps badly and
   stops being legible, it doesn't read as "confident." */
h1, h2, h3 { font-family: var(--font-display); font-weight: 700; letter-spacing: -0.02em; line-height: 1.03; color: var(--ink); }
/* Bumped 2026-08-14 ("push the limits" pass, client-directed) — was
   clamp(36px,5.2vw,72px). Section headlines are the main rhythm-setter for
   how bold the whole page reads at a glance; pushed the ceiling up without
   raising the floor much, so short 1-line headlines get noticeably bigger
   at desktop width while long ones (several sub-service H1s run 8-10
   words) don't start overwrapping. */
/* Ceiling dropped 80px -> 56px (2026-08-24 #2). The hero H1 tops out at
   62px, so an 80px section headline made every H2 on the page LARGER than
   the page's own headline — a straight hierarchy inversion, and on the
   rebuilt Why Trionova section it rendered a four-line 80px wall directly
   under the hero. The previous ceiling was set in the 2026-08-14 "push the
   limits" pass against a hero whose H1 ran to 104px; the hero has since been
   rebuilt smaller and this never followed it down. */
h2 { font-size: clamp(32px, 4.2vw, 56px); }
h3 { font-size: clamp(22px, 2.4vw, 30px); font-weight: 600; }
/* Dialed back from clamp(52px,8vw,132px) after checking real content, not
   just Home's short headline — sub-service pages pull their H1 verbatim
   from the source doc's hero headlines, several of which run 8-10 words
   ("24/7/365 Expert Database Management & Proactive Operational Support"),
   and at the original scale that wrapped to 5+ lines in the ~820px hero
   column, pushing the rest of the page excessively far down. This scale
   still reads meaningfully bigger than the pre-redesign clamp(44px,7vw,96px)
   on every page, without the long-copy pages overwrapping. */
.hero-h1 { font-size: clamp(46px, 6.6vw, 104px); letter-spacing: -0.03em; line-height: 0.95; }

/* ===== Numbered system label (2026-08-14) — replaces the plain pill-chip
   eyebrow as the primary section marker: "01 — SECTION NAME" in mono,
   uppercase, wide tracking, with a thin rule trailing off to the right —
   the same "technical documentation" register Nexdeep3 uses
   (SYSTEM 01 / ENGINEERING OVERVIEW) and appropriate for an infra/database
   consulting brand specifically (reads as engineering rigor, not
   SaaS-generic). eyebrow-chip (pill) stays in the system for tighter
   contexts (nav-adjacent, card-level) — this is the section-level one. */
.section-label { display: flex; align-items: center; gap: 16px; font-family: var(--font-mono);
  font-size: 12px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--accent-1);
  margin-bottom: 20px; }
.section-label-num { color: var(--muted); }
.section-label-rule { flex: 1; height: 1px; background: var(--border-dark); min-width: 40px; }
/* P0 (impeccable audit, 2026-08-24 #2). The blanket
   `[data-theme="light"] .section-label` rules further down this file apply to
   EVERY label in light theme, including the two that live inside
   `.section-dark` bands (background #0A0C10, unconditionally dark by design).
   Measured there: label 1.64:1, number 1.04:1 — the "04" on The Process was
   an unreadable smudge. `.section-dark` already re-scopes --accent-1/--muted/
   --border-dark to their dark values, so pointing these back at the tokens is
   both the fix and the thing that stops it recurring: any future light-theme
   override of a token-driven property needs a .section-dark counterpart.

   2026-08-24 #3 — that earlier fix did NOT work, and the reason is in its own
   last sentence: it claimed (0,2,0) beats the [data-theme] rules' "(0,1,1)".
   `[data-theme="light"] .section-label` is one attribute selector plus one
   class = (0,2,0), exactly the same specificity, so source order decides and
   the light-theme rule three lines BELOW won. Re-measured live in light
   theme: still 1.64:1 on The Process and Direct Access, unchanged since the
   day it was "fixed". Written now as `[data-theme="light"] .section-dark ...`
   (0,3,0) so it genuinely outranks the blanket rule in both directions, and
   the plain `.section-dark` rule stays for the dark-theme/no-attribute
   case. */
.section-dark .section-label,
[data-theme="light"] .section-dark .section-label { color: var(--accent-1); }
.section-dark .section-label-num,
[data-theme="light"] .section-dark .section-label-num { color: var(--muted); }
.section-dark .section-label-rule,
[data-theme="light"] .section-dark .section-label-rule { background: var(--border-dark); }
.bg-light .section-label, section.bg-light .section-label, [data-theme="light"] .section-label { color: var(--accent-on-light-text); }
.bg-light .section-label-num, section.bg-light .section-label-num, [data-theme="light"] .section-label-num { color: var(--muted-light); }
.bg-light .section-label-rule, section.bg-light .section-label-rule, [data-theme="light"] .section-label-rule { background: var(--border-light); }
/* Client-reported (2026-08-31), STILL flagged after a first attempt: "Why
   Trionova text is not readable/visible." First pass tried a lighter blue
   (#7DA6FF, matching .presence-section's own dark-background label) —
   still blue-on-blue against this specific section's busy animated dot-
   lattice, still not enough by the client's own follow-up look. Every
   other element that reads cleanly against this exact lattice (the H1,
   the body copy) is white or near-white, not blue — matched that pattern
   instead of trying yet another shade of blue, plus a tight dark
   text-shadow so it holds up over the brightest lattice nodes too, not
   just the average background. */
[data-theme="light"] .hero-whyus-shell.section-dark .section-label {
  color: #F4F7FF;
  text-shadow: 0 1px 3px rgba(4,7,16,0.85), 0 0 14px rgba(4,7,16,0.55);
}
[data-theme="light"] .hero-whyus-shell.section-dark .section-label-rule { background: rgba(244,247,255,0.35); }

/* Mixed solid/outline emphasis (2026-08-14) — one or two words within a
   headline rendered as an outlined/hollow treatment against the solid
   weight around them (mini-bees-wheels technique). Used sparingly, never a
   whole headline. */
.text-outline { color: transparent; -webkit-text-stroke: 2px var(--accent-1); }

/* Reusable accent text color that's already AA-safe in both themes (2026-08-
   14) — accent-1 alone clears 4.5:1 against the dark theme but not the
   light one (~4.16:1, tuned for dark specifically); every other place this
   needed fixing so far was a one-off inline style="color:var(--accent-1)"
   patched individually. Use this class instead of that inline pattern going
   forward so new instances don't reintroduce the same contrast bug. */
.text-accent { color: var(--accent-1); }
[data-theme="light"] .text-accent { color: var(--accent-on-light-text); }

.on-light h1, .on-light h2, .on-light h3, section.bg-light h1, section.bg-light h2, section.bg-light h3 { color: var(--ink-light); }

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

::selection { background: var(--accent-1); color: #06110D; }

/* ===== Skip link ===== */
.skip-link {
  position: absolute; left: 16px; top: -60px; z-index: 200;
  background: var(--accent-1); color: #06110D; padding: 10px 18px; border-radius: 8px;
  font-family: var(--font-mono); font-size: 13px; transition: top 0.2s ease;
}
.skip-link:focus { top: 16px; }

:focus-visible { outline: 2px solid var(--accent-1); outline-offset: 3px; }
.on-light :focus-visible, section.bg-light :focus-visible { outline-color: var(--accent-on-light); }

/* ===== Load-in FOUC guard =====
   [data-reveal] (2026-08-13, client-reported "GSAP flickering" — real bug,
   confirmed) had NO guard here at all until now — only [data-load] did.
   motion.js's ScrollTrigger.batch('[data-reveal]', ...) never sets an
   initial hidden state via JS (unlike [data-load], which gets an explicit
   gsap.set(...,{autoAlpha:1}) at load-sequence start, implying it starts
   hidden) — it only applies the {y:40, autoAlpha:0} from-state the instant
   a batch's onEnter actually fires, which for anything already in the
   initial viewport happens right after the JS module loads, not before
   first paint. Every data-reveal element sat fully visible — every card,
   heading block, kanban card — until GSAP caught up and snapped it hidden
   before animating back in. Matches a previously-documented failure mode on
   another project (Asam & Co. Perfumers, 2026-08-13): a selector with
   animation logic but no CSS guard is exactly this bug waiting to happen. */
.js [data-load], .js [data-reveal] { visibility: hidden; }

/* motion.js's initLoadSequence() also individually animates .hero-eyebrow,
   .hero-h1 .line, .hero-rotator, and .hero-cta directly (not via
   [data-load]/[data-reveal]) on every page template. .hero-eyebrow needs no
   guard of its own — its only visible content is the eyebrow() component's
   inner span, which already carries [data-load] and is guarded above. The
   other three had NONE: their actual text/button content isn't wrapped in
   any separately-guarded element, so they sat fully visible until GSAP
   caught up, then snapped hidden before animating in — same bug as
   [data-reveal] above, just on different selectors. Each guard here
   matches exactly what GSAP itself animates for that target (see the
   guard() calls in motion.js), not a generic visibility:hidden — using
   visibility:hidden for .hero-rotator/.hero-cta would leave them stuck
   permanently invisible, since their tweens only ever set `opacity`, never
   `visibility`, back to visible. .hero-h1 .line animates a pure `transform`
   (no opacity component at all), so its guard pre-sets the same
   translateY(110%) GSAP's own "from" state uses — clipped out of view by
   .line-wrap's `overflow: hidden` — rather than visibility/opacity, for the
   same reason. */
.js .hero-h1 .line { transform: translateY(110%); }
.js .hero-rotator, .js .hero-cta { opacity: 0; }

/* ===== Layout primitives ===== */
.wrap { max-width: 1240px; margin-inline: auto; padding-inline: 24px; }
.section { position: relative; padding-block: clamp(96px, 12vw, 160px); overflow: clip; }
.section.bg-alt { background: var(--bg-alt); }
.section.bg-light { background: var(--bg-light); color: var(--ink-light); }
.section.bg-transparent { background: transparent; }

/* ===== Dark statement section (2026-08-14, real redesign pass) =====
   Every section on Home was reading as the same pale off-white band, one
   after another — the previous "push the limits" pass added bolder cards
   and bigger type WITHIN that flat rhythm but never questioned the rhythm
   itself, which is why it read as barely different at a glance. This is
   the actual fix: light/dark contrast as a structural device, the way the
   reference site (and virtually every premium marketing site) uses it —
   alternating genuinely dark bands, not just dark cards floating on an
   unbroken light page.
   Re-scopes the SAME theme-toggle custom properties every component in
   this file already reads (--bg, --ink, --muted, --border-dark, --fill-*,
   --surface) to their dark values, inherited down through this section's
   whole subtree — identical mechanism to how [data-theme="light"]
   overrides the root, just scoped to one section instead of the page.
   That means every component built against these tokens (kanban timeline
   dots, section labels, spec strips, icon boxes) automatically renders
   correctly dark here with zero per-component overrides. */
.section-dark {
  --bg: #0A0C10;
  --bg-alt: #0D1015;
  --ink: #F4F6F7;
  --ink-rgb: 244,246,247;
  --muted: rgba(244,246,247,0.7);
  --border-dark: rgba(255,255,255,0.08);
  --fill-1: rgba(255,255,255,0.03);
  --fill-2: rgba(255,255,255,0.05);
  --fill-3: rgba(255,255,255,0.12);
  --fill-4: rgba(255,255,255,0.22);
  --surface: linear-gradient(180deg, rgba(255,255,255,0.055), rgba(255,255,255,0.02));
  --accent-glow: rgba(62,125,245,0.28);
  /* World tokens re-scoped too (2026-08-24 #2) — a .section-dark band can now
     carry schematic chrome (grid overlays, signal chips, mini readouts) and
     must not inherit the LIGHT branch's paper-blue rules onto a black ground. */
  --hero-canvas: #0A0C10;
  --hero-canvas-rgb: 10,12,16;
  --hero-ink: #F4F6F7;
  --hero-ink-rgb: 244,246,247;
  --grid-line: rgba(63,169,245,0.075);
  --grid-line-strong: rgba(63,169,245,0.20);
  --signal-ok: #2FE0B4;
  --signal-ok-rgb: 47,224,180;
  --signal-alert: #F0483D;
  --signal-alert-rgb: 240,72,61;
  --signal-amber: #F0A63D;
  --signal-amber-rgb: 240,166,61;
  background: var(--bg); color: var(--ink);
}

/* ===== Glow / grain / ghost numeral ===== */
.glow { position: absolute; width: 640px; height: 640px; pointer-events: none; z-index: 0;
  background: radial-gradient(closest-side, var(--accent-glow), transparent); }

.grain { position: relative; }
.grain::after { content: ''; position: absolute; inset: 0; pointer-events: none; opacity: 0.04; z-index: 1;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E"); }

/* Stroke follows the ground, not the theme name: light default and any
   .bg-light band get an ink stroke; the dark branch and any .section-dark
   island get a white one. Polarity inverted 2026-08-24 #2 alongside the
   root tokens. */
.ghost-numeral { font-size: clamp(120px, 18vw, 240px); font-weight: 800; color: transparent;
  -webkit-text-stroke: 1px rgba(14,20,26,0.10); position: absolute; user-select: none; z-index: 0; line-height: 1; }
.section-dark .ghost-numeral, [data-theme="dark"] .ghost-numeral { -webkit-text-stroke: 1px rgba(255,255,255,0.10); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .ghost-numeral { -webkit-text-stroke: 1px rgba(255,255,255,0.10); }
}
.bg-light .ghost-numeral, [data-theme="dark"] .bg-light .ghost-numeral { -webkit-text-stroke: 1px rgba(14,20,26,0.10); }

/* ===== Eyebrow chip ===== */
.eyebrow-chip { display: inline-flex; align-items: center; gap: 8px; font-family: var(--font-mono);
  font-size: 11px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--accent-1);
  border: 1px solid rgba(62, 125, 245,0.28); border-radius: 999px; padding: 6px 14px; position: relative; z-index: 2; }
.eyebrow-chip .dot { width: 6px; height: 6px; border-radius: 999px; background: var(--accent-1); flex: none; }
.bg-light .eyebrow-chip, section.bg-light .eyebrow-chip, [data-theme="light"] .eyebrow-chip { color: var(--accent-on-light-text); border-color: rgba(27,69,196,0.28); }
.bg-light .eyebrow-chip .dot, section.bg-light .eyebrow-chip .dot, [data-theme="light"] .eyebrow-chip .dot { background: var(--accent-on-light); }
/* 2026-08-14, Aigocy-inspired redesign — the reference's eyebrow badge is a
   solid floating white pill with a soft shadow, not an outline-only chip.
   Adds a filled backdrop + shadow on top of the existing border (kept, for
   definition against a near-white card background) — light theme only:
   in dark theme the badge already sits on a genuinely dark backdrop where a
   solid near-white fill would clash rather than "float." Scoped to the
   hero eyebrow specifically, not every inline eyebrow-chip use sitewide
   (e.g. the footer's borderless "Quick Links" label reuses the same class
   with an inline border:none override — a global fill would fight that). */
[data-theme="light"] .hero-eyebrow .eyebrow-chip {
  background: #FFFFFF; box-shadow: 0 1px 2px rgba(16,20,26,0.04), 0 10px 24px rgba(16,20,26,0.07); }

/* ===== Cards ===== */
.card { position: relative; border-radius: 16px; padding: 32px;
  background: var(--surface);
  box-shadow: inset 0 1px 0 var(--fill-2);
  transition: transform 0.3s cubic-bezier(0.16,1,0.3,1), border-color 0.3s ease; }
.card::before { content: ''; position: absolute; inset: 0; border-radius: inherit; padding: 1px;
  background: linear-gradient(135deg, var(--fill-4), var(--fill-2) 40%, transparent 65%);
  -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  -webkit-mask-composite: xor; mask-composite: exclude; pointer-events: none; }
.card:hover .card-border-glow { opacity: 1; }

/* Domain hero right-rail tile (2026-09-02, client-directed: "hero right
   side is empty add a something about its Service Areas") — the 5 domain
   pages' hero went to a single centered column on 2026-08-26 once the old
   Engines-Supported/Support-Model spec panel was dropped, leaving the
   right half of the hero blank on wide screens. This restores a two-
   column hero (text left, card right) with a quick-glance list of that
   domain's own services, each linking straight to its sub-service page —
   same links as the .service-table further down, just a compact preview
   up in the hero instead of an empty quadrant. */
/* Two-column hero grid, only for the 5 domain pages that pair this tile
   with the hero text — a dedicated class, not an arbitrary-value Tailwind
   utility (this project's main.css is a pre-compiled/static Tailwind
   output with no live JIT build to generate a one-off class from, so an
   invented `lg:grid-cols-[1.3fr_1fr]` would silently match nothing). */
.hero-domain-grid { display: flex; flex-direction: column; gap: 40px; align-items: flex-start; }
@media (min-width: 1024px) {
  .hero-domain-grid { display: grid; grid-template-columns: 1.3fr 1fr; gap: 64px; align-items: center; }
}
.hero-services-tile { padding: 36px 40px; }
.hero-services-tile-label { font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--muted); margin-bottom: 22px; }
.hero-services-tile-list { display: flex; flex-direction: column; gap: 4px; list-style: none; }
.hero-services-tile-list li { border-bottom: 1px solid var(--border-dark); }
.hero-services-tile-list li:last-child { border-bottom: none; }
.hero-services-tile-list a { display: flex; align-items: center; gap: 12px; padding: 13px 2px;
  font-size: 14.5px; font-weight: 600; color: var(--ink); }
.hero-services-tile-list a i { flex: none; color: var(--accent-1); }
.hero-services-tile-list a:hover { color: var(--accent-1); }
@media (max-width: 1023px) { .hero-services-tile { margin-top: 8px; } }

/* Domain "Service Areas" table (2026-09-01, client-directed: "add a table
   to minimize the whitespace" — replaces the padded 3-col card grid on
   each of the 5 domain pages with a dense, single-column row list. Same
   link targets/copy as before, just laid out to read as a compact index
   rather than 5-6 separate cards with their own padding/shadow each. */
.service-table { display: flex; flex-direction: column;
  border: 1px solid var(--border-dark); border-radius: 16px; overflow: hidden;
  background: var(--surface); }
.service-table-row { display: flex; align-items: center; gap: 28px;
  padding: 20px 28px; border-bottom: 1px solid var(--border-dark);
  transition: background 0.2s ease; }
.service-table-row:last-child { border-bottom: none; }
.service-table-row:hover { background: var(--fill-2); }
.service-table-title { flex: 0 0 260px; font-weight: 600; font-size: 15px; color: var(--ink); }
.service-table-desc { flex: 1 1 auto; font-size: 14px; color: var(--muted); }
.service-table-arrow { flex: none; color: var(--muted);
  transition: transform 0.2s ease, color 0.2s ease; }
.service-table-row:hover .service-table-arrow { color: var(--accent-1); transform: translateX(3px); }
@media (max-width: 780px) {
  .service-table-row { flex-direction: column; align-items: flex-start; gap: 6px; padding: 18px 20px; }
  .service-table-title { flex: none; }
  .service-table-arrow { display: none; }
}

.card-light { position: relative; border-radius: 16px; padding: 32px;
  background: var(--surface-light); border: 1px solid rgba(14,20,26,0.07);
  box-shadow: 0 1px 2px rgba(0,0,0,0.04), 0 16px 40px rgba(0,0,0,0.06);
  transition: transform 0.3s cubic-bezier(0.16,1,0.3,1), border-color 0.3s ease; }
.card-light:hover { border-color: rgba(27,69,196,0.3); }
/* 2026-08-14, real redesign pass — .card-light is now used inside
   .section-dark (Direct Access), which re-scopes --ink to a light value
   for the whole section so its OWN headline/body text reads correctly
   white-on-dark. A card-light card is a white surface floating inside
   that dark section, not part of its background — its own heading needs
   to stay forced-dark regardless of what --ink resolves to in whatever
   section it's dropped into, same reasoning as .kanban-card forcing white
   text unconditionally for the mirror-image case. */
.card-light h3, .card-light h4 { color: var(--ink-light); }
/* .card-light's default border (rgba(14,20,26,0.07), near-black) and
   shadow (rgba(0,0,0,...)) are both tuned to read against a LIGHT section
   — on .section-dark's near-black background a near-black border is
   invisible and a black shadow has nothing darker to cast onto. Swaps to
   a light hairline for edge definition and a deeper/wider shadow for
   actual separation from the section behind it. */
.section-dark .card-light { border-color: rgba(255,255,255,0.08);
  box-shadow: 0 1px 2px rgba(0,0,0,0.25), 0 28px 60px rgba(0,0,0,0.5); }

/* icon container */
.icon-box { width: 48px; height: 48px; border-radius: 12px; display: flex; align-items: center; justify-content: center;
  background: rgba(62, 125, 245,0.12); color: var(--accent-1); flex: none; }
.bg-light .icon-box, .card-light .icon-box { background: rgba(27,69,196,0.10); color: var(--accent-on-light); }
.icon-box-lg { width: 56px; height: 56px; border-radius: 14px; }

/* Trust cards (Direct Access section, 2026-08-14 "push the limits" pass) —
   border color + a real lift on hover, on top of the existing .spotlight
   cursor glow, so the plainest cards on the page get the same "alive"
   feel the rest of the redesign has. Gated to real pointer devices, same
   sitewide convention as every other hover-lift (see the consolidated
   block near the end of this file). */
/* .card's own "border" is a gradient pseudo-element (::before, masked to a
   1px ring), not a real `border` property — a `border-color` transition on
   .trust-card itself would be a no-op. Uses a real border here instead,
   independent of that trick, since this is the one card on the page that
   needs a hover-visible color change, not just the shared subtle gradient
   edge every .card already has. */
.trust-card { border: 1px solid transparent; }
@media (hover: hover) and (pointer: fine) {
  .trust-card { transition: transform 0.3s cubic-bezier(0.16,1,0.3,1), border-color 0.3s ease, box-shadow 0.3s ease; }
  .trust-card:hover { border-color: rgba(62,125,245,0.35); box-shadow: 0 20px 44px rgba(16,20,26,0.1); }
  /* On .section-dark (Direct Access), the default hover shadow above
     (rgba(16,20,26,...), near-black) has nothing darker to show against —
     a blue-tinted glow reads as "lifting toward the light" instead. */
  .section-dark .trust-card:hover { box-shadow: 0 20px 48px var(--accent-glow); }
}

/* Stats section tiles (2026-08-14, "push the limits" pass) — bounds each
   number in a bordered box instead of leaving it floating bare on the
   section background, matching every other number treatment on the page
   (spec strips, service-panel numerals). */
.stat-tile { padding: 28px 24px; border-radius: 16px; border: 1px solid var(--border-dark);
  background: var(--fill-1); transition: border-color 0.3s ease, transform 0.3s ease; }
@media (hover: hover) and (pointer: fine) {
  .stat-tile:hover { border-color: rgba(62,125,245,0.3); transform: translateY(-4px); }
}

/* ===== Buttons ===== */
/* 2026-08-14, Aigocy-inspired redesign — border-radius bumped from 12px to a
   full pill (999px). The reference's whole button/chip/nav language is
   pill-shaped; DESIGN.md's radii scale already had "pills 999px" reserved
   for badges/tags only, buttons at 12px — widening that to buttons too is
   the single highest-leverage change for reading as "that look" without
   touching layout, since every CTA sitewide runs through this one rule. */
.btn { display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  font-family: var(--font-display); font-weight: 600; font-size: 15px; padding: 14px 28px;
  border-radius: 999px; transition: 0.25s cubic-bezier(0.16,1,0.3,1); cursor: pointer; white-space: nowrap; }
/* Solid --accent-2, not a gradient, and white text — not #06110D like the old
   teal button. A top-to-bottom gradient between accent-1 (bright) and
   accent-2 (deep) can't guarantee one text color clears 4.5:1 at every point
   along it (checked: black only clears the accent-1 end, white only the
   accent-2 end) — teal's two stops were close enough in lightness that this
   never came up, blue's aren't. Solid accent-2 + white sidesteps it (7.2:1
   throughout) while the inset highlight still gives it a lit, not-flat feel. */
/* Client-reported (2026-08-14): "black text hard to read on blue background"
   — real bug, confirmed. This rule set `color: var(--ink)` despite the
   comment above it explicitly stating white text; var(--ink) is dark in the
   light theme, so every primary button rendered near-black text on the deep
   accent-2 blue there (dark theme's --ink is near-white, which is why this
   wasn't visible in dark theme screenshots). Fixed to match what the
   comment always said. */
.btn-primary { background: var(--accent-2); color: #FFFFFF;
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.25), 0 8px 24px var(--accent-glow); border: none; }
.btn-primary:hover { filter: brightness(1.1); box-shadow: inset 0 1px 0 rgba(255,255,255,0.3), 0 12px 32px var(--accent-glow); }
.btn-primary:active { filter: brightness(0.96); transform: translateY(0); box-shadow: inset 0 1px 0 rgba(255,255,255,0.2), 0 4px 12px var(--accent-glow); }
.btn:disabled, .btn[disabled] { opacity: 0.5; cursor: not-allowed; transform: none; filter: none; }

/* Icon-pill CTA (2026-08-26, client-directed — matches techorbitgroup.com's
   "Talk to Our Expert"/"Contact us" buttons exactly: a white pill, dark
   text, a filled circular arrow badge on the leading edge). Scoped to the
   nav CTA (shared chrome, every page) and the home hero's primary CTA —
   NOT swapped in for every .btn-primary sitewide, which stays the plain
   solid-blue pill everywhere else; that's a much larger sitewide button
   restyle nothing in this feedback round asked for. */
.btn-icon-pill { background: #fff; color: #0A0E1A; padding: 5px 24px 5px 5px;
  box-shadow: 0 8px 24px rgba(0,0,0,0.3); border: none; }
.btn-icon-pill:hover { filter: none; box-shadow: 0 10px 28px rgba(0,0,0,0.38); }
.btn-icon-pill-icon { display: inline-flex; align-items: center; justify-content: center; flex: none;
  width: 34px; height: 34px; border-radius: 50%; background: var(--accent-2); color: #fff; }
.btn-ghost { border: 1px solid var(--border-dark); color: var(--ink); background: transparent; }
.btn-ghost:hover { border-color: var(--accent-1); color: var(--accent-1); }
.btn-ghost:active { background: var(--fill-1); }
.bg-light .btn-ghost, .card-light .btn-ghost { border-color: var(--border-light); color: var(--ink-light); }
.bg-light .btn-ghost:hover, .card-light .btn-ghost:hover { border-color: var(--accent-on-light); color: var(--accent-on-light-text); }
.bg-light .btn-ghost:active, .card-light .btn-ghost:active { background: rgba(14,20,26,0.04); }
.btn-wa { background: #25D366; color: #fff; border: none; }
.btn-wa:hover { filter: brightness(1.08); }
.btn-wa:active { filter: brightness(0.94); transform: translateY(0); }

/* ===== Links ===== */
.link-underline { position: relative; color: var(--ink); border-bottom: 1px solid var(--accent-1); padding-bottom: 1px; transition: color 0.2s ease; }
.link-underline:hover { color: var(--accent-1); }
.bg-light .link-underline, .card-light .link-underline { color: var(--ink-light); border-color: var(--accent-on-light); }
.bg-light .link-underline:hover, .card-light .link-underline:hover { color: var(--accent-on-light-text); }

/* ===== Navbar ===== */
/* Floating glassmorphism nav (2026-08-13, client reference images) —
   supersedes the earlier edge-to-edge solid-alpha nav (spec Section 6 called
   for 0.92→0.97 flat alpha specifically BECAUSE backdrop-filter was off the
   table then; the client has now asked for actual frosted glass by name and
   reference image, so this is a deliberate, direct override of that older
   note, not a regression of it). Inset on all sides so it reads as a bar
   floating over the page rather than docked to the viewport edge, real
   backdrop-blur so whatever scrolls underneath it stays genuinely visible
   through the panel — the bleed-through problem the old note worked around
   is the point now, not the bug. */
/* ===== Nav (2026-08-26, round 3 — client-directed: "take the menu section
   directly from techorbitgroup.com's navigation") =====
   Was a floating, inset, rounded glass pill (see build-log.md for that
   system's own history). Replaced with what the reference actually uses: a
   flush, full-width, solid dark-navy bar docked to the viewport top, same
   ground color as the home hero so the two read as one continuous surface,
   not two components. Solid (no backdrop-filter) since it's opaque anyway —
   nothing needs to show through it, on any page, light or dark content
   underneath. Text is hardcoded light (not var(--ink), which is near-black
   under the site's one light edition) because this bar is unconditionally
   dark regardless of page theme, same reasoning the hero already uses. */
.nav { position: fixed; top: 0; left: 0; right: 0; width: 100%;
  z-index: 100; height: var(--nav-h); border-radius: 0;
  background: #0A0E1A;
  border-bottom: 1px solid rgba(255,255,255,0.08);
  box-shadow: 0 8px 30px rgba(0,0,0,0.25);
  color: #fff;
  display: flex; align-items: center;
  transition: background 0.3s ease, box-shadow 0.3s ease; }
.nav.nav-scrolled { background: #0A0E1A; box-shadow: 0 10px 36px rgba(0,0,0,0.4); }
.nav .nav-toggle .hamburger span { background: #fff; }
@media (max-width: 640px) {
  .nav { height: 64px; }
}
.nav-links a { opacity: 1; color: rgba(255,255,255,0.86); transition: opacity 0.25s ease, color 0.25s ease; }
/* Client-reported, real (Changes.docx, 2026-08-30): sub-service links inside
   the open Services flyout ("Managed Database Services" etc.) read as dull
   gray instead of the near-black var(--ink) their own rule (below) sets.
   Root cause: this dimming rule was written to fade OTHER top-level nav
   items (About us/Contact) while one is hovered, but `.nav-links:hover a`
   matches every descendant <a> — including every link inside the flyout
   panel nested under the same .nav-links container — so opening the
   dropdown at all (which requires hovering somewhere inside .nav-links)
   knocked every flyout link down to 0.6 opacity regardless of .nav-dropdown-
   detail a's own full-contrast color. Scoped to direct children only
   (`>`) so it can't reach into the flyout. */
.nav-links:hover > a, .nav-links:hover .nav-dropdown-trigger { opacity: 0.6; }
.nav-links a:hover { opacity: 1 !important; color: #7DA6FF; }
.nav-links a.active { color: #7DA6FF; }

/* ===== Services nav dropdown (2026-08-13, client-directed) =====
   Trigger is a real <button> (not a link — clicking it toggles the panel,
   it doesn't navigate anywhere itself; "Browse all services" inside the
   panel is the actual link for that). Panel opens on :hover/:focus-within
   (works with zero JS — mouse and keyboard-tab both covered) OR
   [aria-expanded="true"] (nav-dropdown.js's click-toggle, the case
   hover/focus-within don't reach: touch). */
/* 2026-09-02, client-reported real bug: hovering the empty gap between the
   logo and "Services" (nowhere near the trigger) was opening the dropdown.
   Root cause: `height: 100%` here resolves against .nav-links's own box,
   which shrinks to its content height (~26px) because .nav (and .nav-links)
   center their flex children instead of stretching them — so .nav-dropdown's
   actual box sat mid-header, not flush with the header's true top/bottom
   edges. The ::after hover-bridge below (positioned at `top: 100%` of THIS
   box, sized to the panel's width, not the trigger's) then landed inside the
   header's own row instead of below it, and stretched left far enough to
   cross the logo-to-Services gap — hovering that invisible strip alone
   opened the panel with no need to ever touch "Services". Pinning the height
   to the real header height makes this box's top/bottom match the header's
   actual edges, so the bridge (and the panel it leads to) only becomes
   hoverable below the header, where it visually appears. */
.nav-dropdown { position: relative; height: var(--nav-h); display: flex; align-items: center; }
/* Hover bridge (2026-08-13, real bug — client: "once I move it goes away").
   .nav-dropdown's own hoverable box is only as tall as the trigger button
   (position:absolute children don't expand a flex parent's box), so the
   16px visual gap between the trigger and the panel below it was empty,
   unhovered space — moving the mouse from "Services" down toward the panel
   crossed that gap and lost :hover on .nav-dropdown before ever reaching
   the panel, closing it mid-move. This invisible pseudo-element exactly
   covers that gap, sized and centered to match the panel (not the much
   narrower trigger), so the hoverable area is continuous from trigger to
   panel regardless of the path the mouse takes between them. */
.nav-dropdown::after { content: ''; position: absolute; top: 100%; left: 50%; transform: translateX(-50%);
  width: min(600px, 86vw); height: 16px; }
.nav-dropdown-trigger { display: inline-flex; align-items: center; gap: 6px; background: none; border: none;
  font-family: var(--font-display); font-size: 15.5px; color: rgba(255,255,255,0.86); cursor: pointer; padding: 0;
  opacity: 1; transition: opacity 0.25s ease, color 0.25s ease; }
.nav-dropdown-trigger:hover, .nav-dropdown:focus-within .nav-dropdown-trigger { opacity: 1 !important; color: #7DA6FF; }
.nav-dropdown-trigger.active { color: #7DA6FF; }
.nav-dropdown-chevron { transition: transform 0.25s ease; }
.nav-dropdown:hover .nav-dropdown-chevron,
.nav-dropdown:focus-within .nav-dropdown-chevron,
.nav-dropdown-trigger[aria-expanded="true"] .nav-dropdown-chevron { transform: rotate(180deg); }

/* Client-reported (2026-08-14): hero H1 text (95px) visibly bleeding
   through the dropdown panel, badly hurting legibility of the domain list
   on top of it — confirmed live via a screenshot and via computed style
   (backdrop-filter: blur(20px) was computing to `none` at test time, and
   even the flat rgba background at 0.98 opacity still let large bold text
   read through faintly enough to be distracting). --glass-bg-panel is
   ONLY used here (not shared with the persistent .nav glass chrome, which
   deliberately stays translucent as a whole-site aesthetic) — a dropdown
   MENU has no reason to show page content behind it, so made it fully
   opaque rather than chasing the blur bug further. backdrop-filter left
   in place as a harmless no-op; it does nothing once the background
   behind it can't be seen at all. */
.nav-dropdown-panel { position: absolute; top: calc(100% + 16px); left: 50%; transform: translateX(-50%) translateY(-8px);
  width: min(640px, 90vw); max-height: min(560px, 70vh); overflow-y: auto; padding: 12px; border-radius: 16px; z-index: 110;
  background: var(--glass-bg-panel); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
  border: 1px solid var(--glass-border); box-shadow: 0 20px 50px rgba(0,0,0,0.5);
  opacity: 0; visibility: hidden; pointer-events: none;
  transition: opacity 0.22s ease, transform 0.22s ease, visibility 0.22s; }
.nav-dropdown:hover .nav-dropdown-panel,
.nav-dropdown:focus-within .nav-dropdown-panel,
.nav-dropdown-trigger[aria-expanded="true"] + .nav-dropdown-panel {
  opacity: 1; visibility: visible; transform: translateX(-50%) translateY(0); pointer-events: auto; }
/* 2026-08-29 rework, client-directed: "Remove the short lines under the
   service telling all the sub services[; o]nce service is hovered it has to
   show all the details on right, instead of down." Two-pane flyout: the 5
   domains as a plain list on the left (icon + name, no teaser line
   anymore), sub-services for whichever one is active shown in a fixed pane
   on the right — replaces the old single-column click/hover-to-expand-DOWN
   accordion entirely. */
.nav-dropdown-flyout { display: flex; align-items: stretch; gap: 4px; }
.nav-dropdown-list { flex: 0 0 214px; display: flex; flex-direction: column; gap: 2px;
  padding-right: 10px; border-right: 1px solid var(--border-dark); }
.nav-dropdown-item { display: flex; align-items: center; gap: 12px; padding: 11px 10px; border-radius: 10px;
  transition: background 0.2s ease; }
.nav-dropdown-item:hover, .nav-dropdown-item:focus-visible, .nav-dropdown-item.is-active { background: var(--fill-2); }
.nav-dropdown-icon { width: 38px; height: 38px; border-radius: 10px; display: flex; align-items: center; justify-content: center;
  flex: none; background: rgba(62,125,245,0.12); color: var(--accent-1); }
.nav-dropdown-item-title { flex: 1; font-size: 14.5px; font-weight: 700; color: var(--ink); line-height: 1.3; }
.nav-dropdown-item-arrow { flex: none; color: var(--muted); opacity: 0.55; transition: transform 0.2s ease, opacity 0.2s ease, color 0.2s ease; }
.nav-dropdown-item:hover .nav-dropdown-item-arrow,
.nav-dropdown-item:focus-visible .nav-dropdown-item-arrow,
.nav-dropdown-item.is-active .nav-dropdown-item-arrow { opacity: 1; color: var(--accent-1); transform: translateX(2px); }

.nav-dropdown-detail-pane { flex: 1 1 auto; min-width: 260px; position: relative; padding-left: 14px; }
.nav-dropdown-detail { display: none; flex-direction: column; gap: 1px; animation: nav-detail-in 0.18s ease; }
.nav-dropdown-detail-label { display: block; padding: 4px 10px 8px; font-family: var(--font-mono); font-size: 11px;
  letter-spacing: 0.06em; text-transform: uppercase; color: var(--muted); }
/* Client-reported (2026-08-14, still true of this rework): sub-service
   link text hard to read at var(--muted) against this panel's translucent
   glass surface — full var(--ink) contrast + 14px, same weight as a real
   navigable link, not a de-emphasized caption. */
.nav-dropdown-detail a { display: block; padding: 8px 10px; border-radius: 8px; font-size: 14px; color: var(--ink); }
.nav-dropdown-detail a:hover { background: var(--fill-2); color: var(--accent-1); }
@keyframes nav-detail-in { from { opacity: 0; transform: translateX(-4px); } to { opacity: 1; transform: none; } }
/* Default pane before anything's been hovered/focused, so opening the
   flyout never shows an empty right half. */
.nav-dropdown-detail[data-detail-idx="0"] { display: flex; }
/* 2026-09-02, client-reported real bug ("scrolling from one to another it
   gets appended and the list becomes long, then I see the correct one —
   flickering"): this used to switch on raw `:hover` AND on `.is-active`
   (set by JS with a short intent delay, see nav-dropdown.js) as two
   INDEPENDENT triggers. The instant the pointer entered a new row, CSS
   `:hover` showed its pane immediately while `.is-active` — deliberately
   delayed so a fast mouse pass-through doesn't hijack the pane — still
   pointed at the PREVIOUS row for another ~180ms. Two different rules,
   two different target panes, no conflict between them, so for that
   window BOTH panes rendered `display:flex` at once, stacked, visibly
   growing the panel's height until the delay caught up and the old one
   disappeared. Fixed by making `.is-active` (mouse/touch, via JS) and
   `:focus-visible` (keyboard-tab, instant — no delay needed since tabbing
   jumps discretely and can't "pass through" a row) the ONLY two triggers;
   plain `:hover` no longer shows a pane on its own, so there is exactly
   one source of truth at all times. Index-paired since a shared ancestor
   (:has()) is the only way for the right pane to know which LEFT row is
   active — add a pair of rules here if a 6th service domain is ever
   introduced. */
.nav-dropdown-flyout:has(.nav-dropdown-item:focus-visible) .nav-dropdown-detail,
.nav-dropdown-flyout:has(.nav-dropdown-item.is-active) .nav-dropdown-detail { display: none; }
.nav-dropdown-flyout:has(.nav-dropdown-item[data-domain-idx="0"]:focus-visible) .nav-dropdown-detail[data-detail-idx="0"],
.nav-dropdown-flyout:has(.nav-dropdown-item[data-domain-idx="0"].is-active) .nav-dropdown-detail[data-detail-idx="0"],
.nav-dropdown-flyout:has(.nav-dropdown-item[data-domain-idx="1"]:focus-visible) .nav-dropdown-detail[data-detail-idx="1"],
.nav-dropdown-flyout:has(.nav-dropdown-item[data-domain-idx="1"].is-active) .nav-dropdown-detail[data-detail-idx="1"],
.nav-dropdown-flyout:has(.nav-dropdown-item[data-domain-idx="2"]:focus-visible) .nav-dropdown-detail[data-detail-idx="2"],
.nav-dropdown-flyout:has(.nav-dropdown-item[data-domain-idx="2"].is-active) .nav-dropdown-detail[data-detail-idx="2"],
.nav-dropdown-flyout:has(.nav-dropdown-item[data-domain-idx="3"]:focus-visible) .nav-dropdown-detail[data-detail-idx="3"],
.nav-dropdown-flyout:has(.nav-dropdown-item[data-domain-idx="3"].is-active) .nav-dropdown-detail[data-detail-idx="3"],
.nav-dropdown-flyout:has(.nav-dropdown-item[data-domain-idx="4"]:focus-visible) .nav-dropdown-detail[data-detail-idx="4"],
.nav-dropdown-flyout:has(.nav-dropdown-item[data-domain-idx="4"].is-active) .nav-dropdown-detail[data-detail-idx="4"],
.nav-dropdown-flyout:has(.nav-dropdown-item[data-domain-idx="5"]:focus-visible) .nav-dropdown-detail[data-detail-idx="5"],
.nav-dropdown-flyout:has(.nav-dropdown-item[data-domain-idx="5"].is-active) .nav-dropdown-detail[data-detail-idx="5"] {
  display: flex;
}
.nav-dropdown-all { display: flex; align-items: center; justify-content: space-between; margin-top: 10px; padding: 12px;
  border-top: 1px solid var(--border-dark); font-family: var(--font-mono); font-size: 12px; letter-spacing: 0.04em;
  color: var(--accent-1); }

/* ===== Nav logo (2026-08-31, client-directed: "fix it as light logo,
   remove the selecting option") =====
   Was a dark/light preview picker (2026-08-29) toggling between two logo
   images via [data-logo-variant] — the client has now picked the light
   variant permanently, so this is just the one logo image on its
   permanent white chip (the light wordmark needs that light backing
   regardless of the nav's own unconditionally-dark background — see
   logoImg()'s forceDark comment in layout.mjs for why the nav itself
   doesn't follow site theme). No picker markup left to select against. */
.nav-logo-group { display: inline-flex; align-items: center; gap: 10px; }
.nav-logo-chip { display: inline-flex; align-items: center; border-radius: 10px; background: #fff; padding: 6px 12px; }
.nav-logo-img { display: block; object-fit: cover; max-height: 100%; isolation: isolate; transform: translateZ(0); }

/* Footer logo — single lockup, no dark/light toggle (the footer's own
   [data-theme="dark"] branch was retired 2026-08-26 along with the rest of
   the dark edition, so the .logo-mark-dark/-light pair that used to live
   here was dead code, never actually reachable). Keeps the same
   compositing hardening as .nav-logo-img (caps the box, isolates its own
   layer) for the same GPU-blur-bleed reason. */
.footer-logo-img { display: block; object-fit: cover; max-height: 100%; isolation: isolate; transform: translateZ(0); }

/* mobile menu — <dialog> (2026-08-14, was a hand-rolled div; migrated for
   native focus-trap/Escape/top-layer per the project's own non-negotiable).
   `dialog` ships its own centered-box/border/padding UA styles and a
   `display:none` default that only lifts once showModal() sets [open] —
   reset all of that back to the fixed full-screen panel this component
   actually is, then drive the slide transition off the .open class (see
   mobile-nav.js for why that's separate from the dialog's own open state). */
.mobile-menu { position: fixed; inset: 0; margin: 0; border: none; z-index: 150; background: var(--bg); color: inherit;
  width: 100%; height: 100%; max-width: none; max-height: none;
  padding: 96px 24px 40px; transform: translateY(-100%); overflow-y: auto;
  transition: transform 0.35s cubic-bezier(0.16,1,0.3,1); }
.mobile-menu[open] { display: flex; flex-direction: column; }
.mobile-menu.open { transform: translateY(0); }
.mobile-menu::backdrop { background: transparent; }
/* impeccable polish finding (2026-08-14): the <dialog> migration's top-layer
   stacking put the panel visually and hit-test above the hamburger that
   opened it, with no backdrop margin to tap either — genuinely unclosable
   on touch without this. */
.mobile-menu-close { position: absolute; top: 24px; right: 24px; display: inline-flex; align-items: center; justify-content: center;
  width: 44px; height: 44px; border-radius: 999px; border: 1px solid var(--border-dark); background: transparent;
  color: var(--ink); cursor: pointer; }
.mobile-menu-label { font-family: var(--font-mono); font-size: 12px; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--muted); margin-top: 8px; }
.mobile-menu-label:first-child { margin-top: 0; }
.mobile-menu-service { font-size: 18px !important; font-weight: 500 !important; color: var(--muted); }
/* Touch-target finding (audit P1, 2026-08-14): text-only links measured
   30-36px tall on real mobile, under the 44px comfortable/24px AA-minimum
   bar — the gap-6 wrapper spaced links apart visually but the links'
   actual hit boxes were still just their line-height. */
.mobile-menu a:not(.btn) { padding-block: 8px; }
.nav-toggle { display: inline-flex; align-items: center; justify-content: center;
  width: 44px; height: 44px; background: transparent; border: none; cursor: pointer; }
.hamburger { display: inline-block; width: 28px; height: 20px; position: relative; }
.hamburger span { position: absolute; left: 0; width: 100%; height: 2px; background: var(--ink); transition: transform 0.3s ease, opacity 0.3s ease; }
.hamburger span:nth-child(1) { top: 0; }
.hamburger span:nth-child(2) { top: 9px; }
.hamburger span:nth-child(3) { top: 18px; }
.hamburger.active span:nth-child(1) { transform: translateY(9px) rotate(45deg); }
.hamburger.active span:nth-child(2) { opacity: 0; }
.hamburger.active span:nth-child(3) { transform: translateY(-9px) rotate(-45deg); }

/* ===== Forms ===== */
.field { display: flex; flex-direction: column; gap: 8px; }
.field label { font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted); }
.field input, .field select, .field textarea {
  background: var(--fill-1); border: 1px solid var(--border-dark); border-radius: 10px;
  padding: 14px 16px; color: var(--ink); font-family: var(--font-display); font-size: 15px;
  transition: border-color 0.2s ease; }
.field input:focus, .field select:focus, .field textarea:focus { outline: none; border-color: var(--accent-1); }
.field textarea { resize: vertical; min-height: 120px; }

/* ===== C3 Cursor spotlight ===== */
.spotlight { position: relative; }
.spotlight::after { content: ''; position: absolute; inset: 0; border-radius: inherit; opacity: 0;
  background: radial-gradient(280px circle at var(--mx, 50%) var(--my, 50%), rgba(62, 125, 245,0.10), transparent 65%);
  transition: opacity 0.3s; pointer-events: none; z-index: 1; }
.spotlight:hover::after { opacity: 1; }
@media (hover: none) { .spotlight::after { display: none; } }
/* grid-template-rows 0fr->1fr instead of max-height: animates a track size
   rather than layout box height, so it doesn't trigger layout thrash the
   way animating max-height/height/padding does. */
.spotlight-reveal { display: grid; grid-template-rows: 0fr; opacity: 0;
  transition: grid-template-rows 0.3s ease, opacity 0.3s ease; }
.spotlight-reveal > * { min-height: 0; }
.spotlight:hover .spotlight-reveal { grid-template-rows: 1fr; opacity: 1; }

/* ===== C4 Border shimmer ===== */
.shimmer-border { position: relative; }
.shimmer-border::before {
  content: ''; position: absolute; inset: 0; border-radius: inherit; padding: 1px;
  -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  -webkit-mask-composite: xor; mask-composite: exclude; pointer-events: none; z-index: 1;
  background: linear-gradient(135deg, rgba(62, 125, 245,0.7), rgba(62, 125, 245,0.05) 30%, transparent 55%, rgba(62, 125, 245,0.5) 80%, rgba(62, 125, 245,0.05));
  background-size: 300% 300%;
  animation: shimmer-sweep 7s ease-in-out infinite; }
@keyframes shimmer-sweep { 0% { background-position: 0% 0%; } 50% { background-position: 100% 100%; } 100% { background-position: 0% 0%; } }

/* ===== C9 Scroll progress ===== */
.scroll-progress { position: fixed; top: 0; left: 0; right: 0; height: 2px; z-index: 110;
  background: linear-gradient(90deg, var(--accent-1), var(--accent-2)); transform: scaleX(0); transform-origin: left; }

/* ===== WhatsApp floater ===== */
.wa-floater { position: fixed; bottom: 24px; right: 24px; width: 56px; height: 56px; border-radius: 50%;
  background: #25D366; z-index: 90; display: flex; align-items: center; justify-content: center;
  box-shadow: 0 8px 24px rgba(37,211,102,0.35); }
.wa-pulse { position: absolute; inset: 0; border-radius: 50%; border: 2px solid #25D366; opacity: 0; }

/* ===== Scroll-to-top ===== */
.scroll-top-btn { position: fixed; bottom: 96px; right: 24px; width: 44px; height: 44px; border-radius: 50%;
  /* hallmark audit finding (2026-08-24): background-color is hardcoded dark
     (rgba(20,24,30,0.9)) unconditionally, but `color` was reading the
     theme-tracking --ink token — which resolves to a near-black value in the
     site's light theme (the default), landing a near-black arrow icon on a
     near-black circle. Confirmed via computed styles: functionally
     invisible. Background stays unconditionally dark on purpose (it needs
     to read against whatever's behind it, which varies section to section);
     the icon color is now the one side of the pair pinned to match it. */
  /* 2026-08-24 #2: the previous fix (--ink -> --hero-ink) did not work and
     the audit caught it by measuring rather than reading the comment.
     --hero-ink inverts too — it is #080D14 in the light branch — so the
     arrow was still ~1.09:1 on its own circle. The background is
     deliberately unconditional-dark, so its foreground pair has to be
     unconditional too. Pinned to literals, not tokens, for that reason. */
  background: var(--surface); background-color: rgba(20,24,30,0.9); border: 1px solid rgba(255,255,255,0.14);
  color: #EDF1F6; z-index: 90; display: flex; align-items: center; justify-content: center;
  opacity: 0; pointer-events: none; transition: opacity 0.3s ease, transform 0.3s ease; }
.scroll-top-btn.visible { opacity: 1; pointer-events: auto; }
.scroll-top-btn:hover { border-color: var(--accent-1); color: var(--accent-1); }

/* ===== Hero H1 line mask ===== */
/* Client-reported (2026-08-14): the "y" in "reliability" wasn't clearly
   visible — real bug. .hero-h1's line-height:0.95 sets a line box shorter
   than the font's actual descender depth, and this wrap's overflow:hidden
   (needed to mask the .line entrance-reveal transform) was clipping
   straight through descenders (y/g/j/p/q) at the tight line-height's exact
   boundary. padding-bottom gives the clip boundary room below the
   baseline; the matching negative margin-bottom cancels it back out of
   layout flow so line spacing between wrapped headline lines is unchanged. */
.line-wrap { overflow: hidden; display: block; padding-bottom: 0.14em; margin-bottom: -0.14em; }

/* ===== C6 Word rotator ===== */
/* impeccable polish finding (2026-08-14): a fixed min-width sized to the
   longest rotating word ("Cloud Infrastructure") left permanent dead space
   after every shorter word — worst on first paint, where "Database" (the
   first word shown) left ~130px of empty gap before "consulting" mid-
   sentence. word-rotator.js now measures and animates the track's actual
   width per word instead; this min-width is only the pre-JS fallback so the
   track isn't literally 0px wide before initWordRotator() runs. */
/* Client-reported (2026-08-14): "not on a straight line, especially the
   changing word" — confirmed via computed styles: this box was 1.3em tall
   (23.4px at the paragraph's 18px font-size) while the surrounding
   paragraph's actual line-height is 1.65em (29.7px, inherited from body).
   The rotating word's own text box renders at the full 29.7px line-height
   regardless of the shorter parent, so `overflow:hidden` was clipping it
   and `vertical-align:bottom` was aligning that clipped box to the wrong
   spot — the word sat at a visibly different baseline than "Expert-led" on
   the same line. Height now matches the line-height exactly. */
.hero-rotator-track { position: relative; display: inline-block; overflow: hidden; vertical-align: bottom;
  min-width: 9ch; height: 1.65em; color: var(--accent-1); }
[data-theme="light"] .hero-rotator-track { color: var(--accent-on-light-text); }
.rotator-word { white-space: nowrap; }

/* Technical schematic frame (2026-08-14, "reimagine the design" — Nexdeep3
   reference: blueprint-style corner brackets around its hero letterform).
   One pseudo-element doing 4 corner brackets via 8 stacked background-image
   layers (2 per corner: a short horizontal + short vertical bar, positioned
   to each of the 4 corners via background-position keywords) — avoids
   needing 4 separate DOM elements for what's a purely decorative frame.
   Originally paired with a second, tiled hairline-grid pseudo-element
   behind it — removed (2026-08-14, impeccable design-hook finding,
   codex-grid-background): a repeating grid-line field is a recognized
   recurring AI-generated-UI signature specifically because independently
   built sites keep converging on the same move, regardless of whether
   there's a real diagram behind it or where the pattern was sourced from.
   That critique held up under review even though this instance sat behind
   an actual network diagram and was derived from a live reference — kept
   the corner brackets, which are a narrower "this is being annotated"
   framing device, not a texture field, and aren't what the finding flagged. */
.hub-frame { position: relative; }
.hub-frame::after { content: ''; position: absolute; inset: -28px; pointer-events: none; z-index: 1; opacity: 0.55;
  background-repeat: no-repeat;
  background-image:
    linear-gradient(var(--accent-1), var(--accent-1)), linear-gradient(var(--accent-1), var(--accent-1)),
    linear-gradient(var(--accent-1), var(--accent-1)), linear-gradient(var(--accent-1), var(--accent-1)),
    linear-gradient(var(--accent-1), var(--accent-1)), linear-gradient(var(--accent-1), var(--accent-1)),
    linear-gradient(var(--accent-1), var(--accent-1)), linear-gradient(var(--accent-1), var(--accent-1));
  background-size: 22px 2px, 2px 22px, 22px 2px, 2px 22px, 22px 2px, 2px 22px, 22px 2px, 2px 22px;
  background-position: 0 0, 0 0, 100% 0, 100% 0, 0 100%, 0 100%, 100% 100%, 100% 100%; }

/* ===== Hub & Spoke Network Diagram (R9v3) — Home hero signature element =====
   Client-directed reference (2026-08-12): a central glowing data core with
   five domain nodes on flowing animated connectors. Real AI-rendered 3D
   imagery (Cloudflare Workers AI / FLUX, generated at build time — see
   scripts/generate-assets.mjs) for the core and node icons; the logo badge,
   connecting lines, glow, and motion stay hand-built HTML/CSS/SVG on top. */
.hub-diagram { position: relative; width: 100%; aspect-ratio: 1 / 1; max-width: 560px; margin-inline: auto; z-index: 2; }
.hub-svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
.hub-line { stroke: var(--accent-1); stroke-width: 1.5; stroke-opacity: 0.55;
  stroke-dasharray: 5 5; animation: hub-dash-flow 1.6s linear infinite; }
.hub-line-glow { stroke: var(--accent-1); stroke-width: 4; stroke-opacity: 0.12; filter: blur(2px); }
@keyframes hub-dash-flow { to { stroke-dashoffset: -20; } }

/* Core — real rendered image, masked circular, with a glow ring and the
   actual client logo overlaid as a precise badge (a diffusion model can't
   reliably reproduce a supplied logo file, so this stays a real asset). */
.hub-core-3d { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  width: 190px; height: 190px; z-index: 2; }
.hub-core-3d-mini { width: 120px; height: 120px; }
/* Mobile QA finding (2026-08-14, impeccable critique P1): at narrow
   viewports the hub-diagram container shrinks well below its 560px max-width
   but the core stayed a fixed 190px, so it visually swallowed the node
   labels positioned by percentage around it (e.g. "APPLICATION & ERP" read
   as "...ICATION & ERP"). Shrinking the core at narrow widths — not the
   node positions, which are shared with the SVG connector-line coordinates
   and would desync the lines from the icons if moved independently —
   restores clearance without touching the diagram's data-driven layout. */
@media (max-width: 480px) {
  .hub-core-3d:not(.hub-core-3d-mini) { width: 128px; height: 128px; }
  .hub-core-3d:not(.hub-core-3d-mini) .hub-core-badge { width: 24px; height: 24px; }
}
.hub-core-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; border-radius: 999px;
  box-shadow: 0 0 44px var(--accent-glow), 0 20px 40px rgba(0,0,0,0.5); }
/* Deliberately hardcoded #0A0C10, not var(--bg) — this badge sits directly
   on top of hub-core.webp, an AI-rendered image with its own solid
   near-black backdrop baked in. Matching the IMAGE's background, not the
   page's, is what makes the badge blend seamlessly; in light theme, a
   var(--bg) badge would be a pale circle floating on a black image — worse,
   not theme-correct. */
.hub-core-badge { position: absolute; left: 50%; bottom: 4%; transform: translateX(-50%); width: 34px; height: 34px;
  border-radius: 999px; border: 2px solid rgba(62, 125, 245,0.55); box-shadow: 0 6px 14px rgba(0,0,0,0.5); background: #0A0C10; }
.hub-core-3d-mini .hub-core-badge { width: 24px; height: 24px; }
.hub-ring { position: absolute; inset: -6px; border-radius: 999px; border: 1px solid var(--accent-1);
  opacity: 0; animation: hub-ping 2.8s ease-out infinite; pointer-events: none; }
.hub-ring-2 { animation-delay: 1.4s; }
@keyframes hub-ping { 0% { transform: scale(1); opacity: 0.5; } 100% { transform: scale(1.5); opacity: 0; } }

/* 2026-09-02, client-directed ("improve the aesthetics of the rotating
   thing") — a small bright satellite tracing its own circle, independent
   of (and faster than) the 34s node-orbit ring, for a layered sense of
   motion depth rather than one single rotation speed reading as static
   once you've seen it for a few seconds. Sized as a ring roughly midway
   between the core and the node orbit; the dot itself sits at one fixed
   point on that ring (12 o'clock) and the whole ring rotates, so the dot
   reads as a point sweeping around the hub. */
.hub-orbit-trace { position: absolute; inset: 18%; border-radius: 999px; pointer-events: none; z-index: 2;
  animation: hub-orbit-trace-spin 9s linear infinite; }
.hub-orbit-trace-dot { position: absolute; left: 50%; top: 0; transform: translate(-50%, -50%);
  width: 6px; height: 6px; border-radius: 50%; background: var(--accent-1);
  box-shadow: 0 0 10px 2px var(--accent-glow), 0 0 2px 1px var(--accent-1); }
@keyframes hub-orbit-trace-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .hub-orbit-trace { animation: none; } }
.hero-split-media-diagram .hub-orbit-trace-dot { background: #7DA6FF; box-shadow: 0 0 12px 2px rgba(125,166,255,0.6), 0 0 2px 1px #7DA6FF; }

.hub-node { position: absolute; transform: translate(-50%, -50%); width: 110px; text-align: center; z-index: 3; }
@media (min-width: 640px) { .hub-node { width: 128px; } }
.hub-node-inner { display: flex; flex-direction: column; align-items: center; gap: 8px; }
/* Cast-shadow ellipse beneath each card sells the "hovering above the
   surface" depth the reference cards have. */
.hub-node::after { content: ''; position: absolute; left: 50%; top: 44px; width: 44px; height: 12px;
  transform: translateX(-50%); background: radial-gradient(closest-side, rgba(0,0,0,0.45), transparent 70%);
  z-index: -1; }
.hub-node-icon { width: 64px; height: 64px; border-radius: 16px; overflow: hidden;
  box-shadow: 0 14px 26px rgba(0,0,0,0.5); }
.hub-node-icon img { width: 100%; height: 100%; object-fit: cover; display: block; }
.hub-node-label { font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--muted); line-height: 1.35; }

/* Clickable hero hub nodes (2026-08-14, client-directed: "once the icon is
   clicked it has to take it their service page") — .hub-node-link is the
   same .hub-node box, now rendered as an <a>, so it needs its own
   hover/focus affordance to actually read as clickable rather than just
   decorative. Hover lift is gated to real pointer devices (existing
   sitewide convention — see the "Hover-only lift/shadow effects" comment
   near the bottom of this file); tap targets on touch don't get a stuck
   hover state. Focus ring goes on .hub-node-icon specifically, not the
   whole ~100px .hub-node-link box, so keyboard focus outlines the actual
   visual chip instead of a large mostly-empty rectangle. */
.hub-node-link { cursor: pointer; }
.hub-node-link:focus-visible { outline: none; }
.hub-node-link:focus-visible .hub-node-icon { outline: 2px solid var(--accent-1); outline-offset: 3px; }
@media (hover: hover) and (pointer: fine) {
  .hub-node-link { transition: transform 0.25s cubic-bezier(0.16,1,0.3,1); }
  .hub-node-link:hover { transform: translate(-50%, -50%) scale(1.08); }
  .hub-node-link:hover .hub-node-icon { box-shadow: 0 0 0 3px var(--accent-glow), 0 14px 26px rgba(0,0,0,0.5); }
}

/* Hero scroll cue (2026-08-14, Aigocy-inspired redesign) — a small floating
   pill anchored to the bottom of the hero, echoing the reference site's own
   centered "Scroll for more" affordance. Sits inside .hero-shell, which is
   `position: relative` via the base .section rule, so bottom/left percent
   values resolve against the whole hero section, not just one column —
   deliberately centered under both the copy and diagram columns, not
   scoped to one side. Hidden below 640px: narrow-viewport heroes are
   already visibly tall/scrollable without a cue, and the extra floating
   element there competes with the WhatsApp floater in the same corner. */
.hero-scroll-cue { position: absolute; left: 50%; bottom: 28px; transform: translateX(-50%);
  display: inline-flex; align-items: center; gap: 10px; padding: 10px 18px; border-radius: 999px;
  background: #FFFFFF; border: 1px solid rgba(14,20,26,0.06);
  box-shadow: 0 1px 2px rgba(16,20,26,0.04), 0 10px 24px rgba(16,20,26,0.07);
  font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--muted); z-index: 2; animation: hero-scroll-cue-bob 2.2s ease-in-out infinite; }
[data-theme="dark"] .hero-scroll-cue { background: #1A1F27; border-color: var(--border-dark);
  box-shadow: 0 10px 24px rgba(0,0,0,0.4); }
.hero-scroll-cue-icon { display: inline-flex; color: var(--accent-1); }
@keyframes hero-scroll-cue-bob { 0%, 100% { transform: translateX(-50%) translateY(0); } 50% { transform: translateX(-50%) translateY(6px); } }
@media (prefers-reduced-motion: reduce) { .hero-scroll-cue { animation: none; } }
@media (max-width: 640px) { .hero-scroll-cue { display: none; } }

/* Orbit layer — the hero-scale diagram is static (matches its original,
   client-approved pentagon layout) until the Why Choose Us block scrolls
   into range, at which point it "converts": the connecting lines (drawn to
   fixed coordinates for the static layout — they'd visibly detach from
   moving icons otherwise) fade out and the node layer starts a
   rotate-wrapper + counter-rotate-content orbit (icon+label together, via
   .hub-node-inner, so labels stay upright and legible through the motion,
   not just the icon). See motion.js for the scroll trigger — the line
   fade is GSAP-driven (inline style), not a CSS class: a class + transition
   approach reliably faded .hub-line-glow but not .hub-line despite both
   living in the same selector list, a real reproducible cascade issue, not
   worth chasing further when GSAP setting the style directly just works. */
.hub-node-orbit { position: absolute; inset: 0; z-index: 3; }
.hub-node-orbit.orbit-active { animation: hub-node-orbit-spin 34s linear infinite; }
.hub-node-orbit.orbit-active .hub-node-inner { animation: hub-node-orbit-spin-counter 34s linear infinite; }
@keyframes hub-node-orbit-spin { to { transform: rotate(360deg); } }
@keyframes hub-node-orbit-spin-counter { to { transform: rotate(-360deg); } }
/* Client-directed (Changes.docx, 2026-08-30): turning this orbit on for the
   first time (see index.html's hub-node-orbit markup) — give it the same
   reduced-motion opt-out every other looping animation on the page has. */
@media (prefers-reduced-motion: reduce) {
  .hub-node-orbit.orbit-active,
  .hub-node-orbit.orbit-active .hub-node-inner { animation: none; }
}

/* ===== Hero shell (2026-08-14: un-merged from Why Trionova) =====
   Was a combined hero+why-us sticky scrolly shell; the client asked for Why
   Trionova to become its own standalone two-column section with its own
   diagram instance (see .why-grid below), so this is hero-only now — a
   plain centered grid, no `position: sticky` (that only earned its keep
   pinning a visual across the combined height of two stacked blocks; a
   single hero row doesn't need it). */
.hero-shell { overflow: visible; }
.hero-grid { display: block; }
.hero-copy-block { position: relative; }
.hero-visual-col { position: relative; }
.hero-stack-wrap { position: relative; }
@media (min-width: 1025px) {
  .hero-grid { display: grid; grid-template-columns: 52% 48%; column-gap: 64px; align-items: center; }
}

/* ===== Hero visual blob (2026-08-14, "full freedom" on the right column
   follow-up) — a soft, rotated organic gradient shape behind the hub
   diagram, echoing the reference site's own large color-blob hero art.
   CSS-only (asymmetric border-radius + blur), no new image asset; the
   diagram's own layout/nodes/animation are untouched, this just sits
   behind them at the lowest z-index in the visual column's stacking
   context. Dark theme gets a touch more opacity — the blob reads fainter
   against a genuinely dark backdrop at the same alpha. */
.hero-visual-blob { position: absolute; top: 4%; right: -8%; width: 640px; height: 640px; max-width: 100%;
  background: linear-gradient(135deg, var(--accent-1), var(--accent-2));
  opacity: 0.14; filter: blur(64px); border-radius: 58% 42% 63% 37% / 41% 55% 45% 59%;
  transform: rotate(-9deg); z-index: 0; pointer-events: none; }
[data-theme="dark"] .hero-visual-blob { opacity: 0.22; }
@media (max-width: 1023px) { .hero-visual-blob { display: none; } }

/* ===== Hero trust chip (2026-08-14, same follow-up) — a small floating
   card overlapping the diagram's bottom-left corner, the reference site's
   "floating testimonial card" device repurposed for an already-true,
   already-published claim (Direct Access section, further down this same
   page) rather than an invented quote/stat. Lives inside .hub-frame (not
   .hero-stack-wrap) so its percentage offsets resolve against the
   diagram's own ~560px square, not the wider column it's centered in —
   first version positioned against the column and landed almost directly
   on top of the Licensing node (bottom-center of the pentagon) instead of
   clearing it. left/bottom pull it fully outside the diagram's own box
   (past both the Cloud Infrastructure and Licensing nodes) so it reads as
   a card resting at the corner, not overlapping content. Below 1024px it
   drops out of absolute position into normal flow, stacked under the
   diagram — the hero-visual-col is no longer wide enough there for a
   floating corner card not to overlap or clip. */
.hero-trust-chip { position: absolute; left: -9%; bottom: -13%; z-index: 4;
  display: flex; align-items: center; gap: 12px; padding: 14px 18px; border-radius: 16px;
  background: #FFFFFF; border: 1px solid rgba(14,20,26,0.06);
  box-shadow: 0 1px 2px rgba(16,20,26,0.04), 0 18px 34px rgba(16,20,26,0.1); }
[data-theme="dark"] .hero-trust-chip { background: #1A1F27; border-color: var(--border-dark); box-shadow: 0 18px 34px rgba(0,0,0,0.4); }
.hero-trust-chip-icon { width: 36px; height: 36px; border-radius: 10px; display: flex; align-items: center; justify-content: center;
  flex: none; background: rgba(62,125,245,0.12); color: var(--accent-1); }
.hero-trust-chip-stat { display: block; font-family: var(--font-display); font-weight: 700; font-size: 15px; color: var(--ink); line-height: 1.2; }
.hero-trust-chip-label { display: block; font-size: 12px; color: var(--muted); margin-top: 2px; white-space: nowrap; }
@media (max-width: 1023px) {
  .hero-trust-chip { position: static; margin: 24px auto 0; width: fit-content; }
}

/* ===== Hero sticker cluster (2026-08-14, Aigocy-inspired redesign) =====
   Three small rotated icon tiles floating near the headline — reuses the
   hub diagram's own domain-icon assets rather than new imagery, styled as
   white rounded-square "stickers" the way the reference scatters its own
   app-icon tiles near its hero copy. Desktop/tablet only: below 1024px the
   hero copy column already runs full-width and tight against the viewport
   edge, and three more floating tiles there reads as clutter, not accent,
   on top of the hub diagram that already occupies the same viewport. */
.hero-sticker-cluster { display: none; }
@media (min-width: 1024px) {
  /* top was -44px — with .hero-copy-block starting at ~118px from the page
     top (nav-h 70px + nav-top 18px + the hero's own 48px extra padding)
     and the fixed nav itself occupying y:18-88px, that put the cluster's
     top edge at ~74px: 14px *under* the nav's bottom edge, clipping the
     tiles' tops against the nav bar (caught in a spacing review against
     the reference, 2026-08-14). -18px is the largest negative offset that
     still clears the nav with a small gap. */
  .hero-sticker-cluster { display: flex; gap: 14px; position: absolute; top: -18px; right: -12px; z-index: 2; }
  .hero-sticker { width: 56px; height: 56px; border-radius: 16px; background: var(--surface-light, #FFFFFF);
    border: 1px solid rgba(14,20,26,0.06); box-shadow: 0 1px 2px rgba(16,20,26,0.04), 0 14px 28px rgba(16,20,26,0.1);
    display: flex; align-items: center; justify-content: center; transform: rotate(var(--rot, 0deg)); }
  [data-theme="dark"] .hero-sticker { background: #1A1F27; border-color: var(--border-dark); }
  .hero-sticker img { width: 32px; height: 32px; object-fit: cover; border-radius: 8px; }
  .hero-sticker:nth-child(2) { margin-top: 22px; }
  .hero-sticker:nth-child(3) { margin-top: -10px; }
}

/* ===== Comparison block (C11) ===== */
.compare-row { display: flex; align-items: flex-start; gap: 12px; padding-block: 12px;
  border-bottom: 1px solid var(--border-light); font-size: 15px; }
.compare-row:last-child { border-bottom: none; }
.compare-icon-x { color: rgba(16,20,26,0.35); flex: none; margin-top: 2px; }
.compare-icon-check { color: var(--accent-1); flex: none; margin-top: 2px; }

/* ===== Comparison block (C11) ===== */
.compare-row { display: flex; align-items: flex-start; gap: 12px; padding-block: 12px;
  border-bottom: 1px solid var(--border-light); font-size: 15px; }
.compare-row:last-child { border-bottom: none; }
.compare-icon-x { color: rgba(16,20,26,0.35); flex: none; margin-top: 2px; }
.compare-icon-check { color: var(--accent-1); flex: none; margin-top: 2px; }

/* ===== Why Trionova — Problem vs Solution two-column rebuild (2026-08-14,
   client-directed full restructure, replaces the "The Problem" card +
   side-by-side compare-row approach entirely; reworked same day — "show
   the Chaos... needs to fit in 1 view") =====
   .why-section trims the standard clamp(96px,12vw,160px) section padding
   down substantially — this section's whole point on this pass is
   fitting in roughly one viewport, and that padding alone was ~200px of
   the old layout's height. 50/50 grid with a thin vertical divider and a
   right-pointing arrow flow marker at its top, collapsing to a single
   stacked column below 1025px (divider hidden — a vertical line between
   two stacked-not-side-by-side blocks doesn't read as a divider, just a
   stray rule). */
.why-section { padding-block: clamp(56px, 6vw, 80px); }
.why-grid { display: grid; grid-template-columns: 1fr; gap: 40px; }
@media (min-width: 1025px) {
  .why-grid { grid-template-columns: 1fr auto 1fr; column-gap: 56px; align-items: start; }
}
.why-divider { display: none; }
@media (min-width: 1025px) {
  .why-divider { display: block; position: relative; width: 1px; background: var(--border-dark); align-self: stretch; }
}
.why-divider-arrow { position: absolute; top: -14px; left: 50%; transform: translateX(-50%);
  width: 28px; height: 28px; border-radius: 999px; background: var(--bg); border: 1px solid var(--border-dark);
  color: var(--accent-1); display: flex; align-items: center; justify-content: center; }
.why-title { font-family: var(--font-display); font-weight: 700; font-size: clamp(21px, 2.1vw, 26px);
  line-height: 1.15; color: var(--accent-1); margin-bottom: 10px; }
.why-para { font-size: 15px; color: var(--muted); max-width: 46ch; margin-bottom: 16px; }

/* Problem side — chaosCluster() (home.mjs), the hub diagram's deliberate
   visual opposite: overlapping off-grid icons, a warning badge instead of
   a connected core, severed dashed line fragments. "Show the chaos, don't
   just say it" (client, 2026-08-14) — the old version here was calm,
   evenly-spaced, slightly-rotated cards, which read as tidy no matter what
   the copy above it claimed. Sized to match .why-solution-diagram exactly
   so both columns stay visually balanced.
   .why-diagram-glow (2026-08-14, client: "the dark background is not
   suiting well") — both diagrams' icon tiles and the hub's core render on
   a solid near-black backdrop baked into the AI-rendered images
   themselves (can't recolor those without regenerating the assets); sitting
   directly on the page background with nothing behind them, that read as a
   harsh black void rather than an intentional design element, especially
   in the light theme. A soft radial glow behind each diagram — amber for
   chaos, blue for the solution, tying into the same warning/accent colors
   already used inside each — gives the black tiles atmospheric context
   instead of floating starkly on bare background. */
.why-chaos-wrap { max-width: 340px; margin-inline: auto; position: relative; }
.why-chaos-wrap::before { content: ''; position: absolute; inset: -10%; z-index: 0; border-radius: 999px;
  background: radial-gradient(closest-side, rgba(240,166,61,0.16), transparent 72%); }
.chaos-diagram { position: relative; z-index: 1; width: 100%; aspect-ratio: 1 / 1; }
.chaos-node { position: absolute; width: 92px; text-align: center; }
.chaos-node-icon { width: 50px; height: 50px; border-radius: 13px; overflow: hidden; margin-inline: auto;
  filter: grayscale(0.5) brightness(0.82); opacity: 0.9; box-shadow: 0 10px 18px rgba(0,0,0,0.3); }
.chaos-node-icon img { width: 100%; height: 100%; object-fit: cover; display: block; }
.chaos-node-label { display: block; margin-top: 6px; font-family: var(--font-mono); font-size: 9px;
  letter-spacing: 0.04em; text-transform: uppercase; color: var(--muted); line-height: 1.25; }
.chaos-warning-ring { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  width: 62px; height: 62px; border-radius: 999px; border: 1px dashed var(--signal-amber); opacity: 0.5;
  animation: hub-ping 2.8s ease-out infinite; pointer-events: none; }
.chaos-warning-badge { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  width: 46px; height: 46px; border-radius: 999px; background: rgba(240,166,61,0.14);
  border: 1px solid rgba(240,166,61,0.4); color: var(--signal-amber);
  display: flex; align-items: center; justify-content: center; }
.chaos-break { position: absolute; width: 34px; height: 0; border-top: 2px dashed rgba(240,166,61,0.45); }
.chaos-break-1 { left: 34%; top: 28%; transform: rotate(38deg); }
.chaos-break-2 { left: 58%; top: 60%; transform: rotate(-18deg); }
.chaos-break-3 { left: 18%; top: 52%; transform: rotate(66deg); }

/* Solution side — the full hub diagram as one cohesive, permanently
   orbiting unit (see hubDiagram({orbitActive:true, mini:true}) in
   home.mjs). Client-reported (2026-08-14): "increase spacing in the
   diagram" — real crowding, confirmed: this container is 300-340px vs the
   hero's ~560px, but was reusing the hero's full-size 190px core and
   128px-wide nodes regardless, so the core alone ate ~60% of the width and
   nodes overlapped it and each other. mini:true (existing hubDiagram
   variant) drops the core to 120px; the node/icon/label sizes below scope
   a further reduction to just this smaller diagram, not the hero's. */
.why-solution-diagram { max-width: 340px; margin-inline: auto; margin-bottom: 16px; position: relative; }
.why-solution-diagram::before { content: ''; position: absolute; inset: -10%; z-index: 0; border-radius: 999px;
  background: radial-gradient(closest-side, var(--accent-glow), transparent 72%); }
.why-solution-diagram .hub-diagram { position: relative; z-index: 1; max-width: 100%; }
.why-solution-diagram .hub-node { width: 84px; }
.why-solution-diagram .hub-node-icon { width: 42px; height: 42px; border-radius: 11px; }
.why-solution-diagram .hub-node-label { font-size: 8px; }
/* Checklist switched from a stacked vertical list to wrapped inline chips
   (2026-08-14, same "fit in one view" pass) — 3 stacked lines cost real
   vertical space a wrapped row doesn't. */
.why-checklist { display: flex; flex-wrap: wrap; gap: 8px 18px; }
.why-checklist li { display: flex; align-items: center; gap: 6px; font-size: 13.5px; color: var(--ink); }
.why-checklist i { color: var(--accent-1); flex: none; }

/* Stats row (2026-08-14) — the old "Five fragmented vendors…" callout and
   the 4-cell spec strip, both previously stacked vertically under the
   problem card, now share one horizontal row below the two-column grid per
   the client's explicit positioning instructions, CTA card on the far
   left. .impact-arrow-badge/-label are unchanged from the earlier pass —
   only their container changed (was .impact-arrow, a flex row on its own;
   now .why-stats-cta, one flex item among several in .why-stats-row). */
.why-stats-row { display: flex; flex-wrap: wrap; gap: 20px; margin-top: 32px; align-items: stretch; }
/* 2026-08-14, real redesign pass — was a quiet bordered box the same gray
   as every other surface on the page; the callout it's making ("five
   vendors become one partner") is the whole section's thesis statement,
   and nothing about how it looked said so. Solid gradient fill (same
   accent-1->accent-2 pair as .kanban-card, so it reads as the same
   "featured/solid" language elsewhere on the page, not a one-off) with a
   real glow shadow — this is the first real color-block moment on the
   page after the hero, right at the point a visitor has scrolled just
   past it. */
.why-stats-cta { flex: 1 1 280px; max-width: 340px; display: flex; flex-direction: row; align-items: center; gap: 14px;
  background: linear-gradient(135deg, var(--accent-1), var(--accent-2)); border: none; border-radius: 14px; padding: 18px 20px;
  box-shadow: 0 16px 40px var(--accent-glow); }
.impact-arrow-badge { width: 40px; height: 40px; border-radius: 999px; display: flex; align-items: center; justify-content: center; flex: none;
  background: #FFFFFF; border: none; color: var(--accent-2);
  box-shadow: 0 1px 2px rgba(0,0,0,0.08), 0 10px 20px rgba(0,0,0,0.12); }
.impact-arrow-label { font-size: 14px; color: rgba(255,255,255,0.85); text-align: left; }
.impact-arrow-label strong { color: #FFFFFF; font-weight: 600; }
.why-stats-strip { flex: 2 1 520px; }

/* ===== Precision section (2026-08-24, client-supplied PrecisionSection spec)
   ==========================================================================
   Replaces the card-based staircase above with a direct port of the supplied
   PrecisionSection design — chip pills + a gradient vertical line + a bare
   item list, no cards/icons-per-step/hover/animation — rebuilt in Trionova's
   own stack (vanilla HTML/CSS, no React/Tailwind-v3). Content is the site's
   real 5-step process (see the `pillars` array in process() in home.mjs),
   reshaped from icon+title+body into label+item-list to fit this layout;
   "Mazzard H" is scoped to this section only rather than replacing the
   site's global type (IBM Plex Sans stays everywhere else).

   Two layouts, same content:
   - .precision-staircase (>=640px): pillars absolutely positioned by inline
     left/bottom in vw, each riser one step taller than the last.
   - .precision-mobile-stack (<640px): the same steps as a single column,
     alternating left/right against their own vertical line.
   ========================================================================= */
@font-face {
  font-family: 'Mazzard H';
  font-weight: 400;
  font-style: normal;
  src: url('https://db.onlinewebfonts.com/t/eb5b5ee332420add9a40ee988cb6ac37.woff2') format('woff2'),
       url('https://db.onlinewebfonts.com/t/eb5b5ee332420add9a40ee988cb6ac37.woff') format('woff'),
       url('https://db.onlinewebfonts.com/t/eb5b5ee332420add9a40ee988cb6ac37.ttf') format('truetype');
}
@font-face {
  font-family: 'Mazzard H';
  font-weight: 500;
  font-style: normal;
  src: url('https://db.onlinewebfonts.com/t/875fffdfa62169a0f131e90f37f1faf4.woff2') format('woff2'),
       url('https://db.onlinewebfonts.com/t/875fffdfa62169a0f131e90f37f1faf4.woff') format('woff'),
       url('https://db.onlinewebfonts.com/t/875fffdfa62169a0f131e90f37f1faf4.ttf') format('truetype');
}

.precision-section {
  --precision-line-gradient: linear-gradient(rgb(28, 78, 255), rgb(254, 136, 27) 0%, rgb(172, 36, 255) 25%, rgb(247, 159, 255) 50%, rgb(255, 214, 0) 66%, rgb(254, 136, 27) 84%, rgba(254, 136, 27, 0) 102%);
  font-family: 'Mazzard H', var(--font-display), sans-serif;
  position: relative;
  background-image: url("https://images.higgs.ai/?default=1&output=webp&url=https%3A%2F%2Fd8j0ntlcm91z4.cloudfront.net%2Fuser_38xzZboKViGWJOttwIXH07lWA1P%2Fhf_20260418_125638_553b96dc-a1fd-4b2b-81a9-ed7daa80006e.png&w=1280&q=85");
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  min-height: 100svh;
  box-sizing: border-box;
  padding: clamp(20px, 2.6vw, 44px) clamp(16px, 4vw, 60px);
  gap: clamp(16px, 2.2vw, 32px);
}

/* ---- Header ---- */
.precision-header { display: flex; flex-direction: column; align-items: center; gap: 18px; }
.precision-badge {
  background-color: rgb(249, 249, 249);
  display: flex; align-items: center; gap: 8px;
  font-size: clamp(12px, 0.9vw, 15px); font-weight: 500;
  border-radius: 36px;
  padding: clamp(6px, 0.6vw, 10px) clamp(10px, 1vw, 16px);
  color: rgb(26, 11, 84);
  white-space: nowrap;
}
.precision-badge svg { width: 16px; height: 15px; flex-shrink: 0; }
.precision-heading-wrap {
  display: flex; flex-direction: column; align-items: center;
  max-width: clamp(600px, 55vw, 820px); gap: 10px;
}
.precision-heading { font-size: clamp(22px, 3vw, 40px); font-weight: 500; color: rgb(26, 11, 84); line-height: 1.15; margin: 0; }
.precision-heading-line { display: block; }
@media (min-width: 640px) { .precision-heading-line { white-space: nowrap; } }
.precision-heading-gradient {
  background-image: linear-gradient(90deg, rgb(43, 167, 255), rgb(202, 69, 255) 50%, rgb(254, 136, 27));
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent; color: transparent;
  padding-bottom: 0.2vw;
  display: block;
}
.precision-subtext { font-size: clamp(13px, 1vw, 16px); color: rgb(169, 151, 206); margin: 0; }

/* ---- Pillars container ---- */
.precision-pillars { width: 100%; max-width: 82.292vw; margin: 0 auto; }

/* ---- Desktop staircase (>=640px) ---- */
.precision-staircase { display: none; list-style: none; margin: 0; padding: 0; }
.precision-mobile-stack { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; width: 100%; color: rgb(26, 11, 84); }

/* The step-number badge on the chip — replaces the earlier logo mark. */
.precision-chip-num {
  display: inline-flex; align-items: center; justify-content: center;
  flex-shrink: 0;
  width: 20px; height: 20px;
  border-radius: 50%;
  background-image: linear-gradient(135deg, rgb(43, 167, 255), rgb(202, 69, 255) 60%, rgb(254, 136, 27));
  color: #fff; font-size: 11px; font-weight: 600;
}

@media (min-width: 640px) {
  .precision-mobile-stack { display: none; }
  .precision-staircase {
    display: block;
    position: relative;
    width: 82.292vw; height: clamp(220px, 20vw, 320px);
    color: rgb(26, 11, 84);
  }
  .precision-pillar {
    position: absolute;
    display: flex; flex-direction: column; align-items: center; justify-content: flex-start;
  }
  .precision-chip {
    display: flex; align-items: center; justify-content: center;
    background-image: linear-gradient(135deg, rgb(255, 255, 255), rgba(255, 255, 255, 0.6));
    font-size: clamp(12px, 1vw, 16px); font-weight: 500;
    border-radius: 20px;
    padding: 0.55vw 1.1vw;
    white-space: nowrap;
    gap: 7px;
  }
  .precision-line-wrap { position: relative; display: flex; flex-direction: column; align-items: center; justify-content: flex-end; }
  .precision-items {
    /* 2026-08-27, client-reported misalignment: only `top` was set (no
       `bottom`/height), so this absolutely-positioned column shrank to its
       own content instead of spanning `.precision-line-wrap`'s height —
       `justify-content: space-between` had nothing to distribute across,
       so every item list clumped at the top-left instead of running
       alongside the full gradient line like the chip/line/list are meant
       to read as one unit. Anchoring `bottom` too gives the box the line's
       actual height, so space-between now spreads the items down it. */
    position: absolute; top: 0.4vw; bottom: 0.4vw; left: 1.5vw;
    display: flex; flex-direction: column; gap: 2px;
    font-size: clamp(11px, 0.85vw, 13px); align-items: flex-start; justify-content: space-between;
  }
  .precision-item { padding: 0.32vw 0.7vw; display: flex; align-items: flex-start; }
  .precision-line { background-image: var(--precision-line-gradient); width: 1px; height: clamp(70px, 7.5vw, 120px); }
}

/* ---- Mobile alternating flow (<640px) ---- */
.precision-mobile-pillar { display: flex; flex-direction: column; align-items: flex-start; width: 100%; padding-bottom: 8px; }
.precision-mobile-pillar.is-right { align-items: flex-end; }
.precision-mobile-chip {
  display: inline-flex; align-items: center;
  background-image: linear-gradient(135deg, rgb(255, 255, 255), rgba(255, 255, 255, 0.6));
  font-size: 15px; font-weight: 500;
  border-radius: 20px;
  padding: 10px 18px;
  white-space: nowrap;
  gap: 7px;
}
.precision-mobile-row { display: flex; align-items: stretch; width: 100%; }
.precision-mobile-pillar.is-right .precision-mobile-row { flex-direction: row-reverse; }
.precision-mobile-line {
  width: 1px; flex-shrink: 0;
  background-image: var(--precision-line-gradient);
  margin-left: 22px;
  min-height: 120px;
}
.precision-mobile-pillar.is-right .precision-mobile-line { margin-left: 0; margin-right: 22px; }
.precision-mobile-items {
  display: flex; flex-direction: column;
  padding-left: 20px; padding-top: 8px; padding-bottom: 8px;
  align-items: flex-start;
}
.precision-mobile-pillar.is-right .precision-mobile-items { padding-left: 0; padding-right: 20px; align-items: flex-end; }
.precision-mobile-item { font-size: 14px; color: rgb(100, 80, 160); padding: 8px 0; }

/* ===== Services grid (2026-08-24 #20, full rebuild) =====
   Replaces the tab-explorer (one active dark panel + four hidden tabs) with
   a card per domain, all five always visible, all real sub-service links
   present without a click — see the comment above servicesTeaser() in
   home.mjs for the two structural reasons the explorer was replaced rather
   than re-skinned. Built on .card/.spotlight/.icon-box as-is, so theme
   support (light/dark) and the hover language are inherited, not
   respecified — the previous panel's hardcoded dark background regardless
   of theme is exactly the class of one-off this avoids. */
.services-section { position: relative; }
/* Client-reported, real (Changes.docx, 2026-08-30): "bring this boxed to
   center" — 5 cards in a 3-wide grid left an orphaned 2-card last row
   pinned to the left edge with a big empty gap on the right (CSS Grid packs
   items left-to-right, row by row, with no built-in way to center a short
   last row). Flexbox with justify-content:center centers each wrapped row's
   leftover space automatically, so the 2-card row centers itself with zero
   markup changes — switched from grid to flex-wrap for exactly this. */
.services-grid { display: flex; flex-wrap: wrap; justify-content: center; gap: 20px; }
.services-grid .service-card { flex: 1 1 100%; }
@media (min-width: 640px) {
  .services-grid .service-card { flex: 1 1 calc(50% - 10px); max-width: calc(50% - 10px); }
}
@media (min-width: 1024px) {
  .services-grid { gap: 24px; }
  .services-grid .service-card { flex: 1 1 calc(33.333% - 16px); max-width: calc(33.333% - 16px); }
}
.service-card { display: flex; flex-direction: column; }
.service-card-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px; }
.service-card-count {
  font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--muted);
}
.service-card-title { font-size: 19px; font-weight: 600; letter-spacing: -0.01em; color: var(--ink); margin-bottom: 8px; }
.service-card-teaser { font-size: 14.5px; line-height: 1.55; color: var(--muted); margin-bottom: 20px; }
.service-card-list { display: flex; flex-direction: column; margin-bottom: 20px; }
.service-card-list a {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 10px 4px; border-top: 1px solid var(--border-dark);
  font-size: 13.5px; color: var(--ink);
  transition: color 0.2s ease, padding-left 0.2s ease;
}
.service-card-list li:first-child a { border-top: none; }
.service-card-list a i { flex: none; opacity: 0; transform: translateX(-4px); transition: opacity 0.2s ease, transform 0.2s ease; color: var(--accent-1); }
@media (hover: hover) and (pointer: fine) {
  .service-card-list a:hover { color: var(--accent-1); padding-left: 8px; }
  .service-card-list a:hover i { opacity: 1; transform: translateX(0); }
}
.service-card-cta {
  display: inline-flex; align-items: center; gap: 8px;
  font-family: var(--font-mono); font-size: 12.5px; letter-spacing: 0.02em;
  color: var(--accent-1);
  padding-top: 18px; margin-top: auto; border-top: 1px solid var(--border-dark);
}
[data-theme="light"] .service-card-cta { color: var(--accent-on-light-text); }

/* ===== Numbered rail (About) ===== */
.rail-row { display: flex; gap: 24px; align-items: baseline; padding-block: 28px; border-bottom: 1px solid var(--border-dark); }
.rail-row:last-child { border-bottom: none; }
.rail-num { font-family: var(--font-mono); font-size: 14px; color: var(--accent-1); flex: none; width: 32px; }
[data-theme="light"] .rail-num { color: var(--accent-on-light-text); }

/* ===== Breadcrumb ===== */
.breadcrumb { font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.06em; color: var(--muted); }
.breadcrumb a:hover { color: var(--accent-1); }

/* ===== Footer ===== */
.footer { background: var(--bg); border-top: 1px solid var(--border-dark); padding-block: 64px 32px;
  position: relative; overflow: hidden;
  /* Client-reported (2026-09-01): footer column text reads "dull" — the
     site-wide --muted (rgba(16,20,26,0.68)) is tuned for body copy on
     white cards, but against the footer's own --bg it read low-contrast
     enough to look washed out. Scoped override only, not a global --muted
     change (that value is verified fine everywhere else it's used). */
  --muted: rgba(16,20,26,0.82); }
/* Social row (2026-09-01, client-directed: "add LinkedIn & WhatsApp icons
   with hyperlink in footer"). Small circular chips, same glass-adjacent
   language as .icon-box elsewhere on the site, not raw bare icons.
   2026-09-02 follow-up (client-reported: "should look good and fit
   well") — --fill-2 (5% black tint) against the footer's own near-white
   --bg read as almost no circle at all, just a faint smudge behind the
   glyph. Bumped to a visible --fill-3 tint plus a real hairline border for
   definition, sized up so the glyphs have real breathing room instead of
   nearly touching the edge, and each icon takes ITS OWN brand color on
   hover (LinkedIn blue / WhatsApp green) instead of one shared generic
   accent — reads as "this is LinkedIn" / "this is WhatsApp", not two
   identical buttons that happen to hold different glyphs. */
.footer-social { display: flex; align-items: center; gap: 12px; margin-top: 18px; }
/* Touch-target finding (audit P1, 2026-08-14): footer links measured
   17-21px tall on mobile — the tightest hit boxes on the site. */
.footer a { display: inline-block; padding-block: 4px; }
/* 2026-09-02, client-reported real bug ("keep the icons in middle") — the
   glyphs sat top-left in their circles instead of centered. Root cause:
   `.footer a` above (one class + one type selector) has HIGHER
   specificity than a bare `.footer-social-icon` class rule, so this
   anchor's own `display:inline-flex` (needed for align/justify-content
   centering to do anything at all) was silently losing to `.footer a`'s
   `display:inline-block` — computed display was really `inline-block`,
   centering never applied, and the SVG just sat at its default top-left
   flow position. Scoped as `.footer .footer-social-icon` (two classes)
   to outrank `.footer a` for real, not just source order. */
.footer .footer-social-icon { display: inline-flex; align-items: center; justify-content: center;
  width: 42px; height: 42px; border-radius: 50%; background: var(--fill-3); color: var(--ink);
  border: 1px solid var(--border-dark); padding: 0;
  transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease; }
.footer-social-icon svg { display: block; }
.footer-social-icon:hover { transform: translateY(-2px); border-color: transparent; }
.footer-social-icon[aria-label*="LinkedIn"]:hover { background: #0A66C2; color: #fff; box-shadow: 0 8px 18px rgba(10,102,194,0.35); }
.footer-social-icon[aria-label*="WhatsApp"]:hover { background: #25D366; color: #fff; box-shadow: 0 8px 18px rgba(37,211,102,0.35); }

/* Giant ghost wordmark (2026-08-14, Aigocy-inspired redesign) — the
   reference repeats its brand name as huge, near-invisible type behind the
   footer's social/contact row. font-size in vw so it scales with viewport
   instead of overflowing narrow ones; clip via .footer's own overflow:hidden
   above rather than a fixed max-width, so it always spans edge-to-edge.
   pointer-events:none + aria-hidden — purely decorative, never a tab stop
   and never intercepts clicks on the real footer content it sits behind. */
.footer-ghost-mark { position: absolute; left: 50%; bottom: -6%; transform: translateX(-50%);
  font-family: var(--font-display); font-weight: 700; font-size: 14vw; line-height: 1;
  color: transparent; -webkit-text-stroke: 1px var(--border-dark); white-space: nowrap;
  pointer-events: none; user-select: none; z-index: 0; }
.footer > .wrap { position: relative; z-index: 1; }

/* ===== Clients marquee ===== */
.clients-marquee { overflow: hidden;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
  mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent); }
.clients-track {
  display: flex; align-items: center; gap: 56px; width: max-content;
  animation: clients-scroll 36s linear infinite;
  /* 2026-08-24 #18 — layer promotion for a 36s-and-running animation. Real
     images (vs. the old plain text) also decode in asynchronously, so
     without this the browser can be caught building the compositing layer
     WHILE images are still settling in — the actual mechanism behind the
     "glitchy" report, on top of clients-marquee.js's exact-distance fix
     below for the more visible symptom (the loop seam jumping). */
  will-change: transform;
  backface-visibility: hidden;
}
/* 2026-08-24 #19 — client: full brand colour (not the grayscale-by-default
   pass above), consistent size across every logo, both themes. Each
   vendor's own mark has a completely different natural aspect ratio (a
   wide wordmark like Oracle's vs. a near-square glyph like AWS's) — fixing
   only the height, the previous version's approach, still leaves visibly
   different-width logos marching past. .clients-mark is now a fixed-size
   slot (same footprint for all fourteen) that centres whichever logo sits
   inside it, constrained by BOTH max-height and max-width so it scales
   down to fit the slot without ever stretching or cropping — true uniform
   sizing, not just uniform height. */
.clients-mark {
  display: flex; align-items: center; justify-content: center;
  flex: none;
  width: 104px; height: 40px;
}
.clients-logo { max-height: 32px; max-width: 100%; width: auto; height: auto; }
@keyframes clients-scroll { from { transform: translateX(0); } to { transform: translateX(calc(-1 * var(--clients-distance, 50%))); } }

/* ===== Custom scrollbar (desktop) ===== */
@media (min-width: 1025px) {
  ::-webkit-scrollbar { width: 10px; }
  ::-webkit-scrollbar-track { background: var(--bg); }
  ::-webkit-scrollbar-thumb { background: var(--accent-2); border-radius: 999px; border: 2px solid var(--bg); }
}

/* ===== Accordion (Services hub) ===== */
/* grid-template-rows 0fr->1fr instead of max-height — see the C3 spotlight
   comment above for why. Driven by the card's aria-expanded state, which
   accordion.js already sets — no JS-measured pixel height needed. */
.accordion-panel { display: grid; grid-template-rows: 0fr; transition: grid-template-rows 0.4s cubic-bezier(0.16,1,0.3,1); }
.accordion-card[aria-expanded="true"] .accordion-panel { grid-template-rows: 1fr; }
.accordion-chevron { transition: transform 0.3s ease; }
.accordion-card[aria-expanded="true"] .accordion-chevron { transform: rotate(180deg); }

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

/* Preloader removed sitewide 2026-08-24 (client instruction: no loading
   screen on any page) — see build-log.md. */

/* ===== Hover-only lift/shadow effects (motion QA finding, 2026-08-14) =====
   25 of 26 :hover rules sitewide were ungated for touch — tapping a card or
   CTA button on a touch device fires the hover state (no true "hover" input
   exists), so the element stayed visually lifted/shadowed until the user
   tapped elsewhere. Color/border hover changes stayed on the base
   selectors (harmless if "stuck"); the transform+shadow "lift" effects —
   the ones that actually look stuck — are consolidated here, gated to
   pointer devices that can genuinely hover. */
@media (hover: hover) and (pointer: fine) {
  .card:hover { transform: translateY(-6px); }
  .card-light:hover { transform: translateY(-6px); }
  .btn-primary:hover { transform: translateY(-2px); }
  .btn-wa:hover { transform: translateY(-2px); }
  .scroll-top-btn:hover { transform: translateY(-3px); }
}

/* ============================================================================
   PRECISION-ENGINEERING HERO + INCIDENT (2026-08-24 scrollcraft rebuild)
   ----------------------------------------------------------------------------
   Acts 1-2 of home.mjs's filmic one-shot (heroSection/incidentSection).
   Deliberately unconditional-dark — see the comment on --signal-ok/-alert in
   the :root tokens above. Driven by [data-sc-*] attributes read/written by
   assets/js/vendor/scrollcraft.js (mounted conditionally from main.js); the
   underlying mechanism selectors ([data-sc-cue], .sc-split, etc.) live in
   sc-devices.css, imported at the top of this file — everything below is
   this page's OWN markup styling, not mechanism CSS.
   ========================================================================== *//* ---------------------------------------------------------------------------
   THE WORLD. Every token this block reads (--hero-canvas, --hero-ink,
   --grid-line, --signal-*) now has a value in BOTH theme branches — see the
   root token block at the top of this file. In dark it is a control room; in
   light it is the same instrument read off a blueprint sheet. Nothing below
   hardcodes a colour literal, which is the whole reason the first pass had to
   ship these acts as "unconditional dark": they were built out of
   rgba(255,255,255,x) and #05070A and could not survive a theme flip.
   --------------------------------------------------------------------------- */
.hero-precision {
  position: relative;
  background: var(--hero-canvas);
  color: var(--hero-ink);
}

/* Generated backdrop (Cloudflare Workers AI, FLUX Schnell — see
   assets.config.json entries "world-backdrop-dark"/"-light"). TWO images, not
   one dimmed both ways: the dark ground gets a real photographic cold-aisle
   long exposure, the light ground gets a blueprint elevation of the same
   subject. A single asset cannot do both jobs — a dark photo behind light-mode
   text is a grey smear, and a blueprint on black is invisible. Both are
   masked to the frame edges so the copy column never sits over image detail,
   and both sit under the grid, which is what stitches the photograph into the
   same drawing system as the rest of the site. */
.world-backdrop {
  position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background-image: url("/assets/img/generated/world-backdrop-light.webp");
  background-size: cover;
  background-position: center;
  /* Held low deliberately. The generator renders drafting annotations as
     unreadable pseudo-text; at this weight, under the grid and behind the
     vignette, it resolves as blueprint texture. Anything higher and the eye
     starts trying to read words that are not words — a named AI tell. */
  opacity: 0.11;
  /* Keep the density at the edges and clear the middle third, where the
     headline and the instrument live. A flat overlay would fix contrast
     nowhere and flatten the image everywhere. */
  mask-image: radial-gradient(ellipse 70% 64% at 50% 46%, transparent 6%, #000 96%);
  -webkit-mask-image: radial-gradient(ellipse 70% 64% at 50% 46%, transparent 6%, #000 96%);
}
[data-theme="dark"] .world-backdrop {
  background-image: url("/assets/img/generated/world-backdrop-dark.webp");
  opacity: 0.5;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .world-backdrop {
    background-image: url("/assets/img/generated/world-backdrop-dark.webp");
    opacity: 0.5;
  }
}
.world-backdrop-deep { opacity: 0.09; }
[data-theme="dark"] .world-backdrop-deep { opacity: 0.3; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .world-backdrop-deep { opacity: 0.3; }
}

/* 2026-08-24 #8 — the actual cause of the client's "blank screen while
   scrolling" bug, found in the engine's own source rather than guessed at:
   scrollcraft ONLY reads/warns about stage positioning, it never sets
   `position` itself (deliberately — "you author real HTML, the engine
   drives it", see the vendor header comment). This project's CSS never
   gave `.sc-stage` (the class the engine adds to whichever child matches
   [data-sc-stage]/.sc-stage) a `position: sticky` rule, so `.hero-precision-
   stage` / `.incident-stage` sat in NORMAL DOCUMENT FLOW inside their
   ~190vh/~360vh pinned-height sections — exactly the engine's own
   documented silent-failure mode ("the stage just scrolls by, the copy
   cues correctly against a frame nobody can see, and every automated check
   passes"). Confirmed: `grep sticky main.css` returned nothing for this
   rule before this fix. Effect: the hero's content scrolled off the top
   after one ordinary viewport, then the section's remaining ~90vh of
   reserved pin height scrolled past as a genuinely empty page (not a fade,
   an actual gap) before the incident section's own un-pinned stage
   scrolled into view from the bottom — precisely what the screen recording
   shows. Every earlier round in this session (cue "to"/rOut tuning) was
   real polish on the fade itself but could not have fixed this, because
   the frame those cues fade within was never staying on screen to begin
   with. `top: 0` because the nav is a floating `position: fixed` overlay,
   not a document-flow bar the stage needs to duck under. */
.hero-precision-stage, .incident-stage, .sc-stage {
  position: sticky;
  top: 0;
  /* Without an explicit height a sticky element is only as tall as its own
     content, which would leave the full-bleed background/grid/vignette
     layers (position:absolute; inset:0 against this element) short of the
     viewport instead of covering it while pinned. */
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}
.hero-precision-grid {
  position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background-image:
    linear-gradient(var(--grid-line) 1px, transparent 1px),
    linear-gradient(90deg, var(--grid-line) 1px, transparent 1px);
  background-size: 56px 56px;
  mask-image: radial-gradient(ellipse 84% 66% at 50% 42%, #000 38%, transparent 92%);
  -webkit-mask-image: radial-gradient(ellipse 84% 66% at 50% 42%, #000 38%, transparent 92%);
}
.hero-precision-scanline {
  position: absolute; inset: 0; z-index: 0; pointer-events: none; opacity: 0.5;
  background: repeating-linear-gradient(180deg, var(--hero-scan) 0px, var(--hero-scan) 1px, transparent 1px, transparent 3px);
}
/* A vignette, not a scrim: the ground is already the right colour, this only
   stops the backdrop's brightest corners competing with the copy. */
.hero-precision-vignette {
  position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background: radial-gradient(ellipse 92% 78% at 50% 44%, transparent 42%, var(--hero-vignette) 100%);
}

/* ----- Act 1: the estate at rest ------------------------------------------
   Redesigned 2026-08-24 #2. The first pass put a single centred copy column
   in the middle of the stage with an empty right half — the dated-design
   smell test's "empty hero quadrant", and a hero whose headline was a setup
   line ("It's 3 AM, something broke") rather than a proposition. Now a real
   two-column composition: the claim on the left, the thing the claim is
   about on the right. The 3 AM line moved into Act 2, where the incident it
   opens actually happens. */
/* Reserve a pinned act's scroll height BEFORE the engine runs (2026-08-24 #3).
   scrollcraft assigns `el.style.height = span * 100 + 'vh'` at init (~210ms);
   until then these sections were 100svh, so the entire document below them
   jumped several viewports on first load. Measured against the production
   build with a layout-shift PerformanceObserver: a single shift of 0.1893,
   source SECTION.hero-precision.incident-shell — the whole of the page's CLS.
   home.mjs emits `--sc-span` inline from the same constant it puts in
   `data-sc-span`, so the two can't drift. Gated on .js: without the engine
   there is nothing to pin and the acts should size to their content. */
.js [data-sc-act="pin"] { height: calc(var(--sc-span, 1.5) * 100vh); }

/* ===== Home hero (2026-08-26, round 3 — client-directed, matched to a
   screenshot of techorbitgroup.com's actual hero) =====
   Two-column split: copy left, a real photo card right, over a dark-navy
   ground carrying a collage of layered semi-transparent rectangles — NOT a
   thin grid-line texture (round 2's attempt, rejected: "not a grid... keep
   it exact same as shown"). Six .hero-block divs stand in for the
   reference's overlapping panes rather than one repeating background-image,
   so their sizes/positions/opacities can be tuned individually to match the
   irregular collage in the screenshot. The dot/ring indicator sits to the
   LEFT of the copy column, vertically centered on the whole section — the
   reference's own position — not tucked into the media card's corner like
   round 2 had it. Headline still falls through to the ordinary
   `h1,h2,h3{font-family:var(--font-display)}` rule (not the removed
   BubbledotICG-FinePos dot-matrix font), and the hero stays
   unconditional-dark independent of the (now-removed) site theme, same as
   before. */
.hero-split {
  position: relative;
  min-height: 100svh;
  display: flex;
  align-items: center;
  overflow: hidden;
  /* 2026-08-27 depth pass: two ambient glow blobs layered under the same
     base gradient (colour/text untouched, per client direction — this only
     adds depth behind what was already there). One sits behind the photo
     card (upper-right, matching the accent blue the card's own shadow
     already glows with), the other low-left behind the copy column, so
     neither half of the section reads as a flat plane. */
  background:
    radial-gradient(ellipse 52% 46% at 84% 18%, rgba(94,150,255,0.24), transparent 68%),
    radial-gradient(ellipse 42% 42% at 6% 92%, rgba(62,110,220,0.16), transparent 70%),
    linear-gradient(155deg, #0A0E1A 0%, #0E1730 55%, #142348 100%);
  color: #fff;
  padding-block: calc(var(--nav-h) + 56px) 64px;
}
/* Fine precision-grid texture, same motif as .hero-precision-grid elsewhere
   on the page — fixed-tone (not the theme-flipping --grid-line token) since
   this hero stays unconditional-dark. Masked so it fades toward the copy
   column and reads strongest behind the photo card, an accent rather than
   noise competing with the headline. */
.hero-split-grid {
  position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background-image:
    linear-gradient(rgba(125,166,255,0.07) 1px, transparent 1px),
    linear-gradient(90deg, rgba(125,166,255,0.07) 1px, transparent 1px);
  background-size: 48px 48px;
  mask-image: radial-gradient(ellipse 60% 70% at 78% 45%, #000 30%, transparent 88%);
  -webkit-mask-image: radial-gradient(ellipse 60% 70% at 78% 45%, #000 30%, transparent 88%);
}
.hero-split-blocks { position: absolute; inset: 0; z-index: 0; pointer-events: none; overflow: hidden; }
.hero-block { position: absolute; background: rgba(125,166,255,0.09); }
.hero-block-1 { left: 38%; top: -6%; width: 16%; height: 46%; background: rgba(125,166,255,0.14); }
.hero-block-2 { left: 54%; top: 8%; width: 10%; height: 30%; }
.hero-block-3 { left: 64%; top: -4%; width: 14%; height: 60%; background: rgba(125,166,255,0.12); }
.hero-block-4 { left: 78%; top: 22%; width: 12%; height: 44%; }
.hero-block-5 { left: 90%; top: 0; width: 10%; height: 38%; background: rgba(125,166,255,0.16); }
.hero-block-6 { left: 46%; top: 62%; width: 20%; height: 40%; background: rgba(125,166,255,0.08); }

/* 2026-08-27, client-directed: alternate hero backgrounds for a live
   picker (see hero-bg-picker below) so the client can compare treatments
   before one gets committed to. Variant "a" (current, default) is the
   round-3 navy + block-collage above — untouched. "b" is a new richer
   blueish gradient. "c" restores the earlier thin grid-line motif (round
   2, previously replaced by the block collage) paired with a few
   outline-only blocks for a blueprint feel, distinct from "a"'s filled
   blocks. Review-only: whichever wins gets built for real and this picker
   + the losing variants come back out. */
.hero-split[data-hero-bg="b"] {
  background:
    radial-gradient(ellipse 60% 55% at 18% 12%, rgba(56,189,248,0.30), transparent 65%),
    radial-gradient(ellipse 55% 60% at 88% 88%, rgba(59,91,255,0.32), transparent 68%),
    linear-gradient(135deg, #050914 0%, #0B1A3D 45%, #123072 100%);
}
.hero-split[data-hero-bg="b"] .hero-split-blocks { opacity: 0; }
.hero-split[data-hero-bg="b"] .hero-split-grid { opacity: 0.65; }

.hero-split[data-hero-bg="c"] {
  background: linear-gradient(160deg, #05070F 0%, #0A1226 55%, #0C1830 100%);
}
.hero-split[data-hero-bg="c"] .hero-split-blocks { display: none; }
.hero-split[data-hero-bg="c"] .hero-split-grid {
  mask-image: none; -webkit-mask-image: none;
  background-size: 32px 32px;
  opacity: 0.9;
}
.hero-outline-blocks { position: absolute; inset: 0; z-index: 0; pointer-events: none; overflow: hidden; display: none; }
.hero-split[data-hero-bg="c"] .hero-outline-blocks { display: block; }
.hero-outline-block { position: absolute; border: 1px solid rgba(125,166,255,0.28); background: transparent; }
.hero-outline-block-1 { left: 40%; top: -4%; width: 18%; height: 50%; }
.hero-outline-block-2 { left: 62%; top: 10%; width: 12%; height: 34%; }
.hero-outline-block-3 { left: 80%; top: -2%; width: 14%; height: 58%; }
.hero-outline-block-4 { left: 48%; top: 60%; width: 22%; height: 42%; }

/* 2026-08-27, follow-up: three more restrained, boardroom-appropriate
   options — none of the above three lean toward the loud/decorative end,
   these lean further still. "d" drops color entirely (graphite, no blue
   cast) for a client that wants maximum-neutral. "e" is a deeper, more
   saturated teal-navy for a client that wants "blueish" but richer than
   Aurora Blue. "f" reuses the grid motif from "c" but fine-pitched and
   paired with soft accent nodes instead of blocks — a quieter, more
   technical read than either "c" or the current block collage. */
.hero-split[data-hero-bg="d"] {
  background:
    radial-gradient(ellipse 55% 45% at 82% 8%, rgba(255,255,255,0.05), transparent 72%),
    linear-gradient(165deg, #0A0B0F 0%, #16181D 55%, #1E2128 100%);
}
.hero-split[data-hero-bg="d"] .hero-split-blocks,
.hero-split[data-hero-bg="d"] .hero-split-grid { opacity: 0; }

.hero-split[data-hero-bg="e"] {
  background:
    radial-gradient(ellipse 70% 60% at 78% 18%, rgba(20,130,150,0.30), transparent 65%),
    radial-gradient(ellipse 60% 55% at 12% 92%, rgba(15,70,150,0.32), transparent 70%),
    linear-gradient(150deg, #02060C 0%, #062230 45%, #0A3B58 100%);
}
.hero-split[data-hero-bg="e"] .hero-split-blocks { opacity: 0; }
.hero-split[data-hero-bg="e"] .hero-split-grid { opacity: 0.45; }

.hero-split[data-hero-bg="f"] {
  background: linear-gradient(160deg, #04060D 0%, #081120 55%, #0A1830 100%);
}
.hero-split[data-hero-bg="f"] .hero-split-blocks { display: none; }
.hero-split[data-hero-bg="f"] .hero-split-grid {
  mask-image: none; -webkit-mask-image: none;
  background-size: 22px 22px;
  opacity: 0.4;
}
.hero-grid-nodes { position: absolute; inset: 0; z-index: 0; pointer-events: none; display: none; }
.hero-split[data-hero-bg="f"] .hero-grid-nodes { display: block; }
.hero-grid-node {
  position: absolute; width: 6px; height: 6px; border-radius: 50%;
  background: rgba(125,166,255,0.9);
  box-shadow: 0 0 16px 4px rgba(125,166,255,0.35);
}
.hero-grid-node-1 { left: 52%; top: 18%; }
.hero-grid-node-2 { left: 74%; top: 46%; }
.hero-grid-node-3 { left: 88%; top: 20%; }

/* 2026-08-27, follow-up #2 — one low-lift motion option ("g") and one
   flagship option ("h"). Motion is pure CSS transform/opacity keyframes on
   a few blurred blobs — no JS, no new dependency, GPU-cheap (transform +
   opacity only), and respects prefers-reduced-motion like every other
   animation on this site (see the media query at the bottom of this
   block). This is deliberately NOT run through GSAP/ScrollTrigger: it's an
   ambient, non-scroll-linked loop with no enter/exit state, so a bare CSS
   @keyframes is the actually-low-lift tool for the job. */
.hero-glow-blobs { position: absolute; inset: 0; z-index: 0; pointer-events: none; overflow: hidden; display: none; }
.hero-glow-blob {
  position: absolute; border-radius: 50%; filter: blur(48px);
  opacity: 0.7; will-change: transform;
}
/* 2026-08-27, client-reported: "I do not see any drift" — the first pass
   (4-5% translate, 16-24s duration, blur(64px), opacity 0.5) was real
   motion but too slow/small/soft to actually read as movement in a normal
   glance. Rebuilt bold on request: much larger absolute-px travel (not
   just a % of the blob's own huge size, so it reads the same regardless
   of blob size), under half the duration, tighter blur so the blob's edge
   is a visible thing that visibly relocates, higher opacity, and a wider
   scale swing. Still transform/opacity-only (cheap) and still fully
   disabled under prefers-reduced-motion below — bold, not inaccessible. */
.hero-glow-blob-1 { left: 8%; top: -8%; width: 34%; height: 46%; background: radial-gradient(circle, rgba(56,189,248,0.65), transparent 70%); animation: heroGlowDriftA 8s ease-in-out infinite alternate; }
.hero-glow-blob-2 { left: 58%; top: 10%; width: 30%; height: 42%; background: radial-gradient(circle, rgba(99,102,241,0.6), transparent 70%); animation: heroGlowDriftB 9s ease-in-out infinite alternate; }
.hero-glow-blob-3 { left: 32%; top: 48%; width: 26%; height: 38%; background: radial-gradient(circle, rgba(59,130,246,0.5), transparent 70%); animation: heroGlowDriftA 10s ease-in-out infinite alternate-reverse; }
@keyframes heroGlowDriftA { 0% { transform: translate(0, 0) scale(1); } 50% { transform: translate(90px, -70px) scale(1.25); } 100% { transform: translate(-60px, 60px) scale(0.85); } }
@keyframes heroGlowDriftB { 0% { transform: translate(0, 0) scale(1); } 50% { transform: translate(-85px, 65px) scale(1.2); } 100% { transform: translate(60px, -55px) scale(0.88); } }
@media (prefers-reduced-motion: reduce) { .hero-glow-blob { animation: none; } }

.hero-split[data-hero-bg="g"] {
  background: linear-gradient(150deg, #04070F 0%, #0A1730 50%, #0F2350 100%);
}
.hero-split[data-hero-bg="g"] .hero-split-blocks { opacity: 0; }
.hero-split[data-hero-bg="g"] .hero-split-grid { opacity: 0.5; }
.hero-split[data-hero-bg="g"] .hero-glow-blobs { display: block; }

/* "Signature" — the flagship option: the same layered-gradient technique
   as "b"/"e" (depth without a photo), the fine masked grid at low opacity
   for structure, the slow-drift blobs from "g" retuned to an indigo/violet
   register instead of blue/teal, and the soft glow nodes from "f" for a
   couple of precise accents. Every piece already exists elsewhere in this
   file — the "elevate" move here is restraint in how they're combined, not
   a new decorative element: nothing competes with the headline, and the
   motion is barely perceptible rather than showy. */
.hero-split[data-hero-bg="h"] {
  background:
    radial-gradient(ellipse 58% 50% at 18% 8%, rgba(99,102,241,0.24), transparent 65%),
    radial-gradient(ellipse 55% 55% at 88% 88%, rgba(56,189,248,0.20), transparent 68%),
    radial-gradient(ellipse 40% 35% at 60% 100%, rgba(139,92,246,0.16), transparent 70%),
    linear-gradient(160deg, #05060D 0%, #0A0F26 45%, #12163E 78%, #1B1F4D 100%);
}
.hero-split[data-hero-bg="h"] .hero-split-blocks { opacity: 0; }
.hero-split[data-hero-bg="h"] .hero-split-grid { opacity: 0.32; }
.hero-split[data-hero-bg="h"] .hero-glow-blobs { display: block; }
.hero-split[data-hero-bg="h"] .hero-glow-blob-1 { background: radial-gradient(circle, rgba(99,102,241,0.5), transparent 70%); }
.hero-split[data-hero-bg="h"] .hero-glow-blob-2 { background: radial-gradient(circle, rgba(56,189,248,0.42), transparent 70%); }
.hero-split[data-hero-bg="h"] .hero-glow-blob-3 { background: radial-gradient(circle, rgba(139,92,246,0.4), transparent 70%); }
.hero-split[data-hero-bg="h"] .hero-grid-nodes { display: block; }
.hero-split[data-hero-bg="h"] .hero-grid-node { background: rgba(165,180,255,0.95); box-shadow: 0 0 18px 5px rgba(139,92,246,0.4); }

/* "3D Estate Lattice" — the old Tier-2 WebGL option (2026-08-24, revived
   2026-08-29 as a picker choice, confirmed permanent 2026-08-31).
   webgl-scene.js already runs unconditionally behind every hero variant
   (`data-world-section="hero"` never changed), but every other variant
   paints an opaque background over it, so the canvas has been rendering,
   invisibly, the whole time. This variant is the one that actually lets it
   show through: the --hero-lattice-bg gradient as the pre-WebGL/
   reduced-motion/no-WebGL fallback (never a blank canvas — see the
   capability gates in webgl-scene.js), turned transparent once
   `.webgl-live` lands so the fixed lattice canvas paints the hero ground
   instead. --hero-lattice-bg/--hero-lattice-ground used to be custom
   properties set at runtime by hero-webgl-bg-preview.js (the review-only
   background picker, since removed) — client-directed 2026-08-31: "use
   the 4th color [Deep Indigo] for Lattice background", so that preset's
   two values (gradient + solid ground, from the picker module's own
   PRESETS table) are now baked directly into the var() fallback below
   instead, since nothing overrides the custom property anymore. Always
   solid dark tones, never the theme-flipping --hero-canvas token, because
   .hero-split is deliberately unconditional-dark in every other variant
   and this one shouldn't be the exception. */
.hero-split[data-hero-bg="i"] {
  background: var(--hero-lattice-bg, linear-gradient(160deg, #07061A 0%, #120F35 55%, #1B1750 100%));
}
.hero-split[data-hero-bg="i"] .hero-split-blocks,
.hero-split[data-hero-bg="i"] .hero-outline-blocks,
.hero-split[data-hero-bg="i"] .hero-grid-nodes { display: none; }
.hero-split[data-hero-bg="i"] .hero-split-grid { opacity: 0.18; }
/* 2026-08-29, client-directed: "give a glow effect on in hero for the
   background design." .hero-glow-blobs already existed — the slow-
   drifting blurred blob treatment built for the old "Signature" picker
   option (the most refined of the review-picker backgrounds, "restraint
   in how [the pieces] are combined... nothing competes with the
   headline") — but was explicitly turned off for this variant
   (display:none, below the old picker's plumbing) since the lattice was
   originally just one option among many and didn't need extra glow on
   top. Now that the lattice IS the permanent hero, reusing that same
   tuned treatment here rather than inventing a new one. Same indigo/
   violet/blue register as "Signature" (not the brighter cyan/indigo of
   the "g" option), sitting on TOP of the transparent .webgl-live
   background so it composites as an additional soft glow layer over the
   lattice canvas, not a replacement for it. */
.hero-split[data-hero-bg="i"] .hero-glow-blobs { display: block; }
/* Bumped noticeably past the old "Signature" alphas (0.42/0.36/0.34) — this
   hero's background is busier (grid + a full point/line lattice field)
   than the flat gradients "Signature" was originally tuned against, so the
   same alpha read as barely-there here. Verified live: at the original
   values the glow was indistinguishable from the lattice's own ambient
   blue in a screenshot; this is the level where it actually reads as its
   own layer. */
.hero-split[data-hero-bg="i"] .hero-glow-blob-1 { background: radial-gradient(circle, rgba(99,102,241,0.62), transparent 70%); }
.hero-split[data-hero-bg="i"] .hero-glow-blob-2 { background: radial-gradient(circle, rgba(56,189,248,0.55), transparent 70%); }
.hero-split[data-hero-bg="i"] .hero-glow-blob-3 { background: radial-gradient(circle, rgba(139,92,246,0.52), transparent 70%); }
.webgl-live .hero-split[data-hero-bg="i"] { background: transparent; }
.webgl-live .hero-split[data-hero-bg="i"] .hero-split-grid { opacity: 0.12; }
/* .world-canvas-host's own ::before paints the fixed layer's ground from
   --hero-canvas, which flips light/dark with the SITE theme — right for the
   old theme-aware precision hero, wrong here, where .hero-split is
   unconditional-dark. Without this override, picking this variant on a
   light-theme page load renders the lattice on a near-white ground and
   crushes the hero's white text to near-invisible. :has() scopes the
   override to exactly when this variant is the one showing. */
html.webgl-live:has(.hero-split[data-hero-bg="i"]) .world-canvas-host::before {
  background: var(--hero-lattice-ground, #07061A);
}
/* Same targeted legibility fix as the retired precision hero: a soft dark
   ground behind the copy column only, so the headline/subhead stay legible
   over a deliberately dense point field. Plain black, not a background-
   matched tint — a neutral scrim reads correctly under any of the
   background picker's grounds, not just the default navy. */
.webgl-live .hero-split[data-hero-bg="i"] .hero-split-copy { position: relative; }
.webgl-live .hero-split[data-hero-bg="i"] .hero-split-copy::before {
  content: ''; position: absolute; inset: -8% -6% -10% -8%; z-index: -1; pointer-events: none;
  background: radial-gradient(ellipse 62% 60% at 34% 50%, rgba(0,0,0,0.68), rgba(0,0,0,0) 74%);
}

/* .hero-bg-picker/.hero-bg-picker-stack/.hero-webgl-color-picker/
   .hero-webgl-bg-picker and their swatch/custom-input rules removed
   (2026-08-31, client-directed): "Remove the color option in the hero" —
   all three review-only pickers (the original 9-way background-style
   picker, already unused in markup before this change, plus the lattice
   accent-color and lattice-background pickers) are gone now that final
   colors are chosen. See the markup comment where the pickers used to sit
   in index.html, and the .hero-split[data-hero-bg="i"] comment above for
   where the chosen values (emerald accent, indigo ground) now live. */
/* Visually-hidden but announced label — for icon-only buttons and other
   controls that need an accessible name without visible text. */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0;
}
/* Wider than the site's shared .wrap (max-width:1240px) — at that width
   the hero's two columns only filled about three-quarters of the bar,
   leaving large, obviously-empty margins on both sides (client-reported:
   "so much gap on both sides... wasting so much space"). 1400px plus a
   smaller inter-column gap gives the copy column enough real width for the
   headline to sit one phrase per line without wrapping mid-phrase. */
.hero-split .hero-split-inner {
  position: relative; z-index: 1;
  width: 100%;
  max-width: 1400px;
  display: flex;
  align-items: center;
  gap: clamp(28px, 4vw, 56px);
}
.hero-split-copy { position: relative; flex: 1 1 620px; min-width: 0; }
/* ===== Sticky hero diagram (2026-08-29, client-directed) =====
   "The hero had scrolling effect... it was good... check n get that
   please." Restores the shape of the original sticky-column hero (built
   5bc1bc6/480e7e6, 2026-08-13, since overwritten) — the right-column
   diagram pins via plain CSS position:sticky while the left column
   scrolls past it — WITHOUT restoring the "converts to a mini diagram"
   half, since the mini diagram (and the Why Choose Us content it lived
   in) was itself removed at later, separate client direction; see the
   comment on heroSection() in home.mjs for the full history. This is
   real position:sticky, not a scroll-jack or pin library, same as the
   original — it needs genuine extra height in the row to have anything
   to stick against, which is what .hero-whyus-copy's two stacked blocks
   (hero copy + the Why Trionova intro, moved up into this same column)
   provide.
   Desktop-only: overridden back to normal static flow below 900px (see
   the existing .hero-split-inner column-stack breakpoint) — a short
   phone viewport gives a sticky element no meaningful room to stick
   within, and .hero-split-media already has its own mobile aspect-ratio
   override there. */
/* .hero-split's base rule sets overflow:hidden (to contain the blurred
   decorative glow blobs/blocks within the section's own box) — but ANY
   overflow:hidden/clip/scroll/auto ancestor between a position:sticky
   element and the real scrolling viewport silently breaks stickiness
   entirely (this project's own documented reliability constraint, hit
   before on this exact page — see the "hero + Why Choose Us" build-log
   entries). Relaxed to visible only on this shell variant; the decorative
   elements are all position:absolute; inset:0 (confined to the section's
   own box by their positioning, not just by clipping) so the only real
   risk is a blurred glow's few px of blur radius bleeding past the edge —
   checked live, none of them sit near the section's (now much taller)
   bottom edge. */
.hero-whyus-shell { overflow: visible; }
.hero-split-inner.hero-whyus-grid { align-items: flex-start; }
.hero-whyus-copy { flex: 1 1 620px; min-width: 0; display: flex; flex-direction: column; gap: 140px; }
.hero-whyus-copy > .hero-split-copy { flex: none; }
.hero-whyus-intro { max-width: 640px; position: relative; }
/* 2026-09-22, client-directed: "the hero section and why us section looks
   like it is put together... doesn't feel like a different section" — a
   hairline seam was tried first and rejected ("remove the line, see if you
   can improve it"). This is the second pass: give the Why-Us block its own
   soft spotlight instead of a boundary — the exact .glow component every
   OTHER page's hero already opens with (see e.g. business-continuity's
   `<div class="glow" style="top:-10%; right:-5%;...">`), just not
   previously used in THIS hero since it already had its own separate decor
   system (lattice canvas, grid, blocks). Sits behind the text (the
   `.relative` wrapper around the copy comes after this glow in DOM, so it
   paints on top per normal stacking order — same technique those other
   hero + .glow pairings already rely on) and reads as "a new area, its own
   light" rather than a hard edge. Hero and Why-Us still share one
   continuous background/lattice by design — the sticky diagram column
   needs the combined height of both blocks to have scroll room against
   (see the comment above .hero-whyus-copy), so this stays a purely
   decorative addition, not a structural split. */
/* Default --accent-glow (0.28 alpha) reads as indistinguishable from the
   lattice's own ambient blue here, same finding the lattice variant's own
   drifting blobs already ran into (see the "Bumped noticeably past the old
   Signature alphas" comment on .hero-split[data-hero-bg="i"] .hero-glow-blob-1
   above) — bumped to match that same precedent instead of the plain .glow
   default. */
.hero-whyus-glow {
  top: -90px; left: -80px; width: 480px; height: 480px;
  background: radial-gradient(closest-side, rgba(99,102,241,0.55), transparent);
}
@media (max-width: 900px) {
  .hero-whyus-glow { top: -50px; left: -50px; width: 320px; height: 320px; }
}
/* Two classes, not one — .hero-split-media's own base rule (position:
   relative, defined further down this file) is equal-specificity single-
   class, and CSS resolves ties by source order, so a single-class
   .hero-whyus-sticky rule here would lose regardless of which one loads
   last. Verified live: computed position came back "relative" until this
   was upgraded to match. */
.hero-split-media.hero-whyus-sticky {
  position: sticky;
  top: calc(var(--nav-h) + 32px);
  align-self: flex-start;
  /* 2026-09-22, client-directed: "rotating image needs to be resized so it
     comes on page 1 only... should be visible completely when we are at
     homepage." At its fixed 460px basis (aspect-ratio 1/1.12 from the base
     .hero-split-media rule -> ~515px tall) the card fit fine on tall
     screens but its bottom edge landed right at or past the fold on
     shorter ones (confirmed at 1366x768 live: card bottom exactly at
     viewport edge, bottom orbit nodes clipped) — real laptop viewports are
     often shorter than 768 once browser chrome is subtracted. Card width
     (flex-basis) is capped so height = width*1.12 never exceeds the space
     actually left below the section's own top padding
     (var(--nav-h) + 56px) plus a small bottom buffer, on every screen
     height, instead of assuming a screen tall enough for the fixed 460px.
     Still resolves to 460px on any screen tall enough to fit it (nothing
     changes on the >=900px-tall screens this was tuned on 2026-08-31). */
  flex: 1 1 min(460px, calc((100svh - var(--nav-h) - 96px) / 1.12));
  max-width: min(460px, calc((100svh - var(--nav-h) - 96px) / 1.12));
}
@media (max-width: 900px) {
  /* .hero-split-inner already goes align-items:stretch + column direction
     at this breakpoint (existing rule above) — just disabling sticky and
     tightening the gap here. */
  .hero-whyus-copy { gap: 64px; }
  /* flex-basis:auto added 2026-08-31 (real bug): .hero-split-media's own
     plain-class mobile override (below, `aspect-ratio:16/10; flex-basis:
     auto`) loses to THIS compound-class rule's `flex:1 1 460px` on
     specificity regardless of source order (0-2-0 beats 0-1-0) — so
     flex-basis stayed pinned at 460px here even on mobile. In column flow
     that 460px sets this card's HEIGHT directly (flex-basis is a main-axis
     size), overriding what the aspect-ratio would have computed from the
     already-stretched width — a card taller than its own aspect-ratio
     implies, which let the height-sized hub-diagram overflow the card's
     actual (narrower) width. */
  .hero-split-media.hero-whyus-sticky { position: relative; top: auto; align-self: stretch; flex-basis: auto; }
}
/* Soft spotlight seated behind the headline — gives the copy column its own
   quiet glow instead of sitting flat against the section background, same
   restraint as the media card's glow (low-opacity, blurred, decorative). */
.hero-split-copy::before {
  content: ''; position: absolute; z-index: -1; pointer-events: none;
  left: -12%; top: -18%; width: 75%; height: 75%;
  background: radial-gradient(closest-side, rgba(94,150,255,0.16), transparent 72%);
  filter: blur(4px);
}
/* Smaller than the base .hero-h1 clamp (tuned for a 1-2 line headline
   elsewhere on the site) — this hero's headline runs 3 lines. white-space:
   nowrap on each .line forces "Innovation That Transforms." etc. to stay
   one phrase per line at any width the clamp lands on, rather than
   wrapping mid-phrase the way plain word-wrap did at this column width
   (client-reported: "Innovation That Transforms. can be in one line"). */
.hero-split-h1 { color: #fff; font-size: clamp(28px, 2.9vw, 46px); line-height: 1.16; }
.hero-split-h1 .line { white-space: nowrap; }
/* .hero-split-accent (blue third line) removed 2026-08-27, client-directed
   ("Solutions That Elevate. to in white as other two") — all three
   headline lines now share the plain .hero-split-h1 white, no per-line
   color split. */
.hero-split .hero-rotator { color: rgba(255,255,255,0.78); }
/* 2026-08-27 polish pass, client-directed ("hero section look for
   enhancement... has to be professional") — copy/background/palette were
   confirmed fine as-is, so this only touches the media card's own
   presentation: a blue-tinted glow riding alongside the black drop shadow
   (a flat black shadow on a navy ground reads a little flat/generic on its
   own), two corner-bracket accents borrowed from the site's existing
   precision-engineering visual language (the same motif the domain/
   sub-service spec panels use), and a bottom scrim so the photo's own
   edges settle into the section instead of ending in a hard rectangle. */
.hero-split-media {
  position: relative;
  flex: 1 1 460px;
  /* Was 4/3 (landscape) — too short to fit the square hub-diagram at its
     current (2026-08-31, client-directed "bigger") size without the
     diagram overflowing the card vertically; see the sizing-mechanism
     comment on .hero-split-media-diagram .hub-diagram below. Slightly
     taller than wide gives the diagram's height-based sizing enough room
     to reach close to its intended max-width without needing to shrink. */
  aspect-ratio: 1 / 1.12;
  border-radius: 20px;
  overflow: hidden;
  border: 1px solid rgba(255,255,255,0.16);
  /* Third, wider glow layer added on top of the existing black-drop +
     tight-blue-glow pair — reads as the card catching light from the same
     ambient blob now sitting behind it in .hero-split's own background,
     rather than the card and backdrop looking like two unrelated layers. */
  box-shadow:
    0 32px 64px rgba(2,5,14,0.55),
    0 0 0 1px rgba(62,125,245,0.08),
    0 20px 60px rgba(62,125,245,0.14),
    0 0 120px rgba(94,150,255,0.16);
}
/* Traced double-edge — a hairline ring just outside the card's own border,
   the same "precision instrument" language the corner brackets already
   carry, rather than the card ending in one flat line. */
.hero-split-media::before {
  content: ''; position: absolute; inset: -1px; z-index: 3; pointer-events: none;
  border-radius: inherit;
  box-shadow: 0 0 0 1px rgba(94,150,255,0.22), inset 0 1px 0 rgba(255,255,255,0.08);
}
.hero-split-photo {
  position: absolute; inset: 0;
  width: 100%; height: 100%; object-fit: cover;
}
.hero-split-media-scrim {
  position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background: linear-gradient(180deg, transparent 55%, rgba(4,7,14,0.55) 100%);
}
/* Hub-and-spoke diagram (real AI-rendered imagery, restored 2026-08-29 —
   see the markup comment on .hero-split-media-diagram in index.html for
   the full history). The card no longer holds a `cover`-fit photo, so it
   needs its own fill + centering for whatever content sits inside — a
   calm radial glow echoing the ambient blob already behind the whole hero,
   with the diagram (naturally square, see .hub-diagram below) centered in
   whatever space the 4:3 card actually has. */
/* 2026-09-02, client-directed ("not very visually appealing... improve the
   aesthetics of the rotating thing") — a slow rotating radar sweep, on-brand
   for this site's own "control room / uptime dashboard" direction
   (DESIGN.md's signature move is a live telemetry readout; this is the
   same visual language applied to the hub diagram). Pure CSS: a wide
   conic-gradient wedge, oversized so it always covers the card at any
   rotation, clipped to the card's own rounded rect by .hero-split-media's
   existing overflow:hidden. Independent of the node-orbit's 34s rotation
   — reads as an active scan sweeping the whole panel, not tied to any one
   node's position. */
.hero-split-media-diagram::before {
  content: ''; position: absolute; inset: -20%; z-index: 0; pointer-events: none;
  background: conic-gradient(from 0deg, transparent 0deg, rgba(125,166,255,0.16) 8deg, transparent 22deg, transparent 360deg);
  animation: hero-radar-sweep 7s linear infinite;
}
@keyframes hero-radar-sweep { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .hero-split-media-diagram::before { animation: none; }
}
.hero-split-media-diagram {
  position: relative;
  display: flex; align-items: center; justify-content: center;
  /* Verified live, real bug, updated 2026-08-31: this was 20px 16px 40px
     (top/sides/bottom) — enough clearance for the one node that used to
     sit near an edge (licensing, top:94%, label clipped by this card's
     overflow:hidden). Now that the whole ring rotates (orbit-active), every
     node's fixed radius from center sweeps through every angle over one
     revolution — the node that was safely mid-circle a moment ago is the
     one near the top or bottom edge a few seconds later, and its label
     (kept upright by the counter-rotation on .hub-node-inner, so it always
     extends "down" in screen space regardless of where on the ring the
     node currently sits) needs that same clearance at the TOP now too, not
     just the bottom. Made symmetric and generous enough on every side for
     the bigger 2026-08-31 node/icon sizes (see .hub-node-icon below). */
  padding: 44px 20px;
  /* Client-directed (2026-09-01): "Enhance the background colour for more
     eye catching" — was a near-black navy wash with a subtle 0.16-alpha
     glow; brighter/more saturated blue glow plus a lighter navy base gives
     the card more presence without fighting the node icons' own contrast. */
  background:
    radial-gradient(ellipse 75% 70% at 50% 42%, rgba(62,125,245,0.32), transparent 74%),
    radial-gradient(ellipse 50% 45% at 82% 88%, rgba(125,90,245,0.18), transparent 70%),
    linear-gradient(165deg, #0A1024 0%, #0F1A3A 55%, #142348 100%);
}
/* Fixed hero-local blue overrides for the shared .hub-diagram rules (see
   the pointer comment above) — .hero-split is deliberately unconditional-
   dark regardless of site theme, but --accent-1/--accent-glow/--muted all
   flip with it, which would read fine in dark theme and wrong in light
   (same bug class the lattice had earlier this build). Uses the exact
   rgba(125,166,255,x)/rgba(94,150,255,x) already shared by every other
   decorative element in this hero (.hero-split-grid, .hero-outline-block,
   .hero-glow-blob, .hero-split-media's own glow) instead of re-deriving a
   new blue. Sized down from the diagram's 560px default to fit this card's
   ~460-620px slot without crowding the corner brackets. */
/* Client-reported (Changes.docx, 2026-08-30): "This image should be bigger"
   — bumped ~15% at every breakpoint (380->440, 430->500px) with core/node/
   icon sizes scaled up to match, same proportions as before.
   2026-08-31 follow-up (client-reported, real bug): that bigger max-width
   let the square .hub-diagram grow WIDER than this card's own height
   (card is a fixed 4:3 landscape rectangle) — so the diagram overflowed
   the card vertically (top AND bottom) regardless of padding, and got
   hard-clipped by .hero-split-media's overflow:hidden. Most visible once
   the orbit animation started actually sweeping every node through that
   overflowing top/bottom band, but the overflow itself predates the
   rotation. Fixed at the source: size the square diagram off the
   container's HEIGHT (a definite value here) instead of its width, so it
   can never exceed the available vertical space no matter how wide the
   card gets — max-width below is now just an upper cap for unusually tall
   containers, not the primary constraint. */
.hero-split-media-diagram .hub-diagram { height: 100%; width: auto; max-width: 440px; }
.hero-split-media-diagram .hub-line { stroke: rgba(125,166,255,0.85); }
.hero-split-media-diagram .hub-line-glow { stroke: rgba(125,166,255,0.85); }
/* Core/node sizes below are the base rule's own px values scaled down to
   this card's smaller container (440-500px vs the base 560px default) at
   the same proportions — an unscaled 190px core would read as ~50% of a
   380px container instead of the intended ~34%. */
.hero-split-media-diagram .hub-core-3d { width: 150px; height: 150px; }
.hero-split-media-diagram .hub-core-img { box-shadow: 0 0 44px rgba(94,150,255,0.35), 0 20px 40px rgba(0,0,0,0.5); }
/* Client-reported (2026-08-31): "Logo size is small increase it, It has to
   be well seen" — was 30/32/22px (≈20% of the core circle's diameter at
   every breakpoint), easy to miss against the core image's own detail.
   Bumped to ≈35% of the core diameter at each breakpoint (52/58/38px) —
   source badge image is 180×180 (apple-touch-icon.png), so this stays
   well within native resolution with room to spare, no upscaling blur. */
.hero-split-media-diagram .hub-core-badge { width: 52px; height: 52px; border-color: rgba(125,166,255,0.55); }
.hero-split-media-diagram .hub-ring { border-color: rgba(125,166,255,0.85); }
.hero-split-media-diagram .hub-node { width: 96px; }
.hero-split-media-diagram .hub-node-icon { width: 50px; height: 50px; border-radius: 12px;
  /* Client-directed (2026-09-01): "you can change the service logo colour
     accordingly" (paired with the brighter card background below) — the
     AI-rendered icons read a little flat against the punchier backdrop
     without a small saturation/contrast lift. */
  filter: saturate(1.35) contrast(1.08) brightness(1.05);
  position: relative; z-index: 1; }
/* 2026-09-02, client-directed ("full freedom... give the best one, keep
   the same style of rotating") — a genuine redesign pass on top of the
   09-01 fixes, not just another tweak:
   1) A soft halo behind each node icon (its own ::before, so it rotates
      WITH the node and stays centered on it) — reads as each node casting
      its own light, instead of a flat dark tile pasted on the card.
   2) A faint dashed orbit-path ring roughly tracing the pentagon the 5
      nodes travel — the missing piece that makes this read as an actual
      orbit diagram rather than "5 icons that happen to move": a track
      the visitor's eye can follow even when a node isn't sitting on it
      at that instant.
   3) A sprinkle of static "starfield" points in the background — cheap
      (box-shadow dots, no images/particles JS) but turns the flat navy
      into something with real depth, on-brand with a control-room/space-
      monitoring read. */
.hero-split-media-diagram .hub-node-icon::before {
  content: ''; position: absolute; inset: -30%; z-index: -1; border-radius: 50%;
  background: radial-gradient(circle, rgba(125,166,255,0.32), transparent 70%);
  filter: blur(2px);
}
.hero-split-media-diagram .hub-diagram::before {
  content: ''; position: absolute; inset: 18%; z-index: 0; border-radius: 50%;
  border: 1px dashed rgba(125,166,255,0.22);
}
.hero-split-media-diagram .hub-diagram::after {
  content: ''; position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background-image:
    radial-gradient(1.5px 1.5px at 12% 22%, rgba(255,255,255,0.55), transparent),
    radial-gradient(1px 1px at 85% 15%, rgba(255,255,255,0.4), transparent),
    radial-gradient(1.5px 1.5px at 92% 68%, rgba(255,255,255,0.5), transparent),
    radial-gradient(1px 1px at 8% 78%, rgba(255,255,255,0.4), transparent),
    radial-gradient(1.5px 1.5px at 48% 8%, rgba(255,255,255,0.35), transparent),
    radial-gradient(1px 1px at 68% 92%, rgba(255,255,255,0.4), transparent);
  animation: hub-starfield-twinkle 5s ease-in-out infinite;
}
@keyframes hub-starfield-twinkle { 0%, 100% { opacity: 0.6; } 50% { opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .hero-split-media-diagram .hub-diagram::after { animation: none; } }
/* Client-directed (2026-09-01): "Increase the text of each revolving
   service lines" — was 9px/8px (mobile), easy to lose against the dark
   card at a glance. ~30% up at both breakpoints. */
.hero-split-media-diagram .hub-node-label { font-size: 12px; color: rgba(255,255,255,0.85); font-weight: 600; }
@media (min-width: 1100px) {
  /* max-width raised 500->560px: at typical widths here the card's own
     height (tuned via .hero-split-media's aspect-ratio) naturally makes
     this diagram ~540-550px tall — a max-width still set to 500 clamped
     the WIDTH there but not the height (aspect-ratio's clamp direction is
     one-way), producing a non-square 500×546 box instead of resizing
     cleanly. 560 sits above what actually gets produced at realistic
     widths, so it's a safety cap again, not an active constraint. */
  .hero-split-media-diagram .hub-diagram { height: 100%; width: auto; max-width: 560px; }
  .hero-split-media-diagram .hub-core-3d { width: 168px; height: 168px; }
  .hero-split-media-diagram .hub-core-badge { width: 58px; height: 58px; }
  .hero-split-media-diagram .hub-node { width: 104px; }
  .hero-split-media-diagram .hub-node-icon { width: 56px; height: 56px; }
}
/* The actual .hub-diagram/.hub-node/etc. rules live in one place, near the
   top of this file (search "Hub & Spoke Network Diagram") — hubDiagram()
   in home.mjs had no call sites since the hero moved to hero-split, so
   that block went dormant but was never deleted. Restoring it here (see
   the .hero-split-hub-diagram override block below for the fixed-color
   adaptation this hero needs) reuses it rather than re-authoring a second,
   competing set of rules under the same class names — kept this comment
   as a pointer since the two blocks now sit ~1300 lines apart. */
.hero-split-media-corner {
  position: absolute; z-index: 4; width: 32px; height: 32px; pointer-events: none;
  border-color: rgba(125,166,255,0.9);
  border-style: solid; border-width: 0;
  filter: drop-shadow(0 0 6px rgba(94,150,255,0.5));
}
.hero-split-media-corner-tl { top: 14px; left: 14px; border-top-width: 2px; border-left-width: 2px; border-top-left-radius: 6px; }
.hero-split-media-corner-br { bottom: 14px; right: 14px; border-bottom-width: 2px; border-right-width: 2px; border-bottom-right-radius: 6px; }
@media (max-width: 900px) {
  .hero-split .hero-split-inner { flex-direction: column; align-items: stretch; text-align: left; }
  /* Real bug found 2026-08-31 while fixing the hub-diagram overflow: in
     row layout flex-basis:460px sets this card's WIDTH (main axis), but
     .hero-split-inner switches to flex-direction:column right above —
     under column flow the SAME flex-basis instead sets HEIGHT to a flat
     460px, ignoring the aspect-ratio below entirely (a definite height +
     a definite cross-axis width from `stretch` leaves aspect-ratio
     nothing to resolve). That mismatched 460px-tall card against its own
     ~327px stretched width let the height-sized square hub-diagram
     (sized off this card's height, see .hero-split-media-diagram
     .hub-diagram) overflow the card's WIDTH instead. flex-basis:auto lets
     aspect-ratio actually drive the height here, so every downstream
     child sized off this card's dimensions gets consistent numbers. */
  /* Was 16/10 (short landscape) — fine for the old photo/plain-media
     variant, but far too short to hold the height-sized square
     hub-diagram at a legible size: a 16:10 card this narrow only leaves
     ~114px for the diagram, well under the 96-104px fixed node width
     the "bigger" 2026-08-30 sizing set, so the 5 node icons overlapped
     each other and their labels. Closer to square gives the diagram
     enough room on phones for its nodes to actually fit apart; the
     node/icon/label sizes are also stepped down for this width just
     below, matching this smaller footprint rather than fighting it. */
  .hero-split-media { aspect-ratio: 1 / 1.05; flex-basis: auto; }
  .hero-split-h1 { font-size: clamp(28px, 8vw, 40px); }
  .hero-split-media-diagram { padding: 28px 16px; }
  .hero-split-media-diagram .hub-core-3d { width: 110px; height: 110px; }
  .hero-split-media-diagram .hub-core-badge { width: 38px; height: 38px; }
  .hero-split-media-diagram .hub-node { width: 74px; }
  .hero-split-media-diagram .hub-node-icon { width: 38px; height: 38px; border-radius: 10px; }
  .hero-split-media-diagram .hub-node-label { font-size: 10px; }
}
/* The mobile margin-top that used to clear .hero-bg-picker-stack (an
   absolutely-positioned overlay that wrapped to multiple rows on narrow
   viewports) is removed along with the picker itself (2026-08-31,
   client-directed) — nothing sits above the copy column to clear anymore. */
@media (max-width: 480px) {
  /* Even the mobile clamp above can outrun a narrow phone's width for the
     longest phrase ("Integration That Connects.") — let those specific
     lines wrap normally there rather than causing horizontal overflow. */
  .hero-split-h1 .line { white-space: normal; }
}

/* Trust row — small avatar-stack + pill sitting above the headline. */
.hero-trust {
  display: inline-flex; align-items: center;
  margin-bottom: 22px;
  /* 34px -> 40px (client-directed, 2026-08-29: "make them lil bigger") */
  --trust-size: 40px;
  /* Client-directed (Changes.docx, 2026-08-30): "Bring this to little
     centre" — was flush against the column's left edge; a small indent
     nudges it inward without fully centering it against the left-aligned
     H1/body copy underneath it. */
  margin-left: clamp(8px, 2vw, 28px);
}
/* 2026-08-29, client-directed: "There can just be 3 icons visible at once,
   It can keep scrolling" — fixed-width viewport showing exactly 3 avatars
   (3 * 40px size - 2 * 16px overlap = 88px; see the step-distance math on
   .hero-trust-avatar-track below) instead of all 6 crammed into one
   overlapping stack. overflow-x hidden clips the sliding track
   horizontally; overflow-y stays visible so a hovered avatar's grow
   animation (below) isn't clipped by the viewport's own height. */
/* Client-reported, real: a plain overflow:hidden edge sliced avatars
   mid-circle as they scrolled through — a visible hard cut, not a clean
   loop. mask-image fades each edge to transparent over the last ~28% of
   the viewport's width, so an avatar dissolves out before it reaches the
   clip boundary instead of being cut off flat ("it has to just flow
   smoothly"). Widened the viewport container slightly (88px -> 96px) to
   give the fade room without eating into the 3-icons-visible width. */
.hero-trust-avatars {
  width: 96px; height: var(--trust-size); overflow-x: hidden; overflow-y: visible;
  -webkit-mask-image: linear-gradient(90deg, transparent 0%, #000 22%, #000 78%, transparent 100%);
  mask-image: linear-gradient(90deg, transparent 0%, #000 22%, #000 78%, transparent 100%);
}
/* Client-reported, real bug: "Once I hover on the icon tile its getting
   cropped." overflow-x:hidden exists to clip the SLIDING track (so only 3
   show at once) — but a hovered avatar growing to 1.55x can extend past
   this 96px box's edge, especially near the left/right third, and got
   hard-clipped by that same overflow, on top of the mask fading it. Since
   hovering ALREADY pauses the track (.hero-trust-avatars-track below), the
   "only 3 visible, seamless loop" illusion has nothing left to protect at
   that exact moment — so :has() relaxes both the clip and the mask the
   instant any avatar inside is hovered, letting it grow (and its label
   show) freely, and restores them the instant the pointer leaves. */
.hero-trust-avatars:has(.hero-trust-avatar:hover) {
  overflow: visible;
  -webkit-mask-image: none;
  mask-image: none;
}
/* Doubled track, seamless loop — same technique clients-marquee.js uses
   further down this page (a second, aria-hidden copy of the same content
   back to back), just pure CSS here since every avatar is identically
   sized (that marquee needs JS to measure real logo-image widths; these
   don't vary). Step between avatar starts is 24px (40px size - 16px
   overlap, i.e. var(--trust-size) * 0.6); 6 avatars per set -> the second
   set starts exactly 144px (6 * 24px) after the first, which is the exact
   distance the animation shifts by, so the loop seam is invisible. */
.hero-trust-avatars-track {
  display: flex; align-items: center;
  animation: heroTrustScroll 11s linear infinite;
}
.hero-trust-avatars:hover .hero-trust-avatars-track { animation-play-state: paused; }
@keyframes heroTrustScroll { from { transform: translateX(0); } to { transform: translateX(-144px); } }
@media (prefers-reduced-motion: reduce) { .hero-trust-avatars-track { animation: none; } }
.hero-trust-avatar {
  position: relative; z-index: 1;
  flex: none;
  width: var(--trust-size); height: var(--trust-size);
  border-radius: 50%;
  background: rgba(20,22,26,0.85);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  border: 1px solid rgba(255,255,255,0.4);
  padding: 4px;
  display: grid; place-items: center;
  box-shadow: 0 4px 14px rgba(0,0,0,0.3);
  transition: transform 0.25s cubic-bezier(0.34,1.56,0.64,1), box-shadow 0.25s ease;
}
.hero-trust-avatar + .hero-trust-avatar { margin-left: calc(var(--trust-size) * -0.4); }
/* "once its hovered, it has to become big" — scales up and rises above its
   overlapping neighbors (z-index) so the grown circle is never clipped
   behind the next avatar in the stack. transform, not width/height, so
   nothing else in the row reflows when it grows. Sized up twice from the
   original 1.4x, both times client-directed ("on hover the thing has to
   become little bigger too", then "little more bigger") — now 1.55x;
   overflow-y:visible on .hero-trust-avatars gives it room regardless. */
.hero-trust-avatar:hover {
  transform: scale(1.55);
  /* Client-directed (Changes.docx, 2026-08-30): tooltip moved from below to
     above the avatar (see .hero-trust-avatar-name below) — that puts it
     right under the fixed .nav (z-index:100). z-index only wins a paint
     order fight against a sibling-of-a-different-ancestor like .nav if it
     out-ranks it in the SAME stacking context both ultimately compete in;
     the old z-index:2 didn't, which is exactly why the label was originally
     placed below instead of fixing this. Bumped past 100 so "above" no
     longer needs that workaround. */
  z-index: 150;
  box-shadow: 0 8px 22px rgba(0,0,0,0.4);
}
.hero-trust-avatar-inner {
  width: 100%; height: 100%; border-radius: 50%;
  background: #fff;
  display: grid; place-items: center;
  overflow: hidden;
}
/* "text has to be on top or below it saying what it is" — a plain labeled
   tooltip, shown on the same :hover that drives the grow effect. Un-scaled
   by the parent's transform (name sits as the avatar's OWN child, so it
   grows right along with it otherwise) via an inverse counter-scale, so
   the label reads at a normal, legible size instead of 1.55x oversized
   mono text.
   ABOVE the circle now (client-directed, Changes.docx 2026-08-30; was below
   for a while — this row sits directly under the fixed nav, z-index:100,
   and an earlier "above" attempt landed underneath the nav's own opaque
   background because the avatar's z-index:2 didn't outrank it in their
   shared stacking context). Fixed properly this time by raising
   .hero-trust-avatar:hover's z-index past the nav's instead of avoiding the
   position — see the comment there. */
.hero-trust-avatar-name {
  position: absolute; left: 50%; bottom: calc(100% + 10px);
  transform: translateX(-50%) scale(calc(1 / 1.55)) translateY(4px);
  transform-origin: bottom center;
  padding: 4px 9px; border-radius: 6px;
  background: rgba(10,12,16,0.92); border: 1px solid rgba(255,255,255,0.14);
  color: #fff; font-family: var(--font-mono); font-size: 11px; white-space: nowrap;
  opacity: 0; pointer-events: none;
  transition: opacity 0.18s ease, transform 0.18s ease;
}
.hero-trust-avatar:hover .hero-trust-avatar-name {
  opacity: 1;
  transform: translateX(-50%) scale(calc(1 / 1.55)) translateY(0);
}
.hero-trust-avatar-inner img { width: 62%; height: 62%; object-fit: contain; }
.hero-trust-pill {
  height: var(--trust-size);
  display: inline-flex; align-items: center;
  background: rgba(20,22,26,0.85);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  border: 1px solid rgba(255,255,255,0.4);
  box-shadow: 0 4px 14px rgba(0,0,0,0.25);
  border-radius: 999px;
  margin-left: calc(var(--trust-size) * -0.4);
  padding-left: calc(var(--trust-size) * 0.55);
  padding-right: 16px;
  font-family: var(--font-mono); font-size: 12.5px; letter-spacing: 0.04em;
  color: rgba(255,255,255,0.8);
  white-space: nowrap;
}
@media (max-width: 480px) {
  /* Fixed nowrap text in a fixed-height pill ran off the right edge of
     narrow phone viewports (spotted on the home hero's trust pill). Let it
     wrap onto a second line there instead of clipping/overflowing. */
  .hero-trust-pill { white-space: normal; height: auto; padding-block: 8px; line-height: 1.3; }
}

/* ----- The stack instrument (used by the "Why Trionova" section below) --- */
.stack-instrument {
  position: relative; z-index: 1;
  border: 1px solid rgba(var(--hero-ink-rgb),0.14);
  border-radius: 18px;
  background:
    linear-gradient(180deg, rgba(var(--hero-ink-rgb),0.045), rgba(var(--hero-ink-rgb),0.015));
  padding: 20px;
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  box-shadow: 0 24px 60px rgba(0,0,0,0.14);
  transform-style: preserve-3d;
}
.stack-instrument-head {
  display: flex; align-items: baseline; justify-content: space-between;
  padding: 0 6px 12px;
  border-bottom: 1px solid rgba(var(--hero-ink-rgb),0.10);
  margin-bottom: 6px;
}
.stack-instrument-title {
  font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.22em; text-transform: uppercase;
  color: rgba(var(--hero-ink-rgb),0.9);
}
.stack-instrument-sub {
  font-family: var(--font-mono); font-size: 10.5px; letter-spacing: 0.1em;
  color: rgba(var(--hero-ink-rgb),0.66); /* was 0.5 — measured 3.60:1 in light */
}
.stack-row {
  display: grid;
  grid-template-columns: auto minmax(0,1fr) auto;
  align-items: center;
  gap: 14px;
  padding: 11px 8px;
  border-radius: 10px;
  color: inherit;
  transition: background-color 0.24s ease;
}
.stack-row + .stack-row { border-top: 1px solid rgba(var(--hero-ink-rgb),0.07); }
.stack-row-num {
  font-family: var(--font-mono); font-size: 10.5px; letter-spacing: 0.08em;
  /* 0.38 measured 2.51:1 at 10.5px in light theme (impeccable audit). The
     world's alphas were all tuned against the dark ground, where the same
     value is fine; on paper they need to be heavier. */
  color: rgba(var(--hero-ink-rgb),0.62);
  font-variant-numeric: tabular-nums;
}
.stack-row-body { display: block; min-width: 0; }
.stack-row-label {
  display: block;
  font-size: 14px; font-weight: 600; line-height: 1.3;
  color: rgba(var(--hero-ink-rgb),0.92);
  margin-bottom: 7px;
}
.stack-row-meter {
  display: block; height: 3px; border-radius: 999px;
  background: rgba(var(--hero-ink-rgb),0.10);
  overflow: hidden;
}
/* Width is proportional to the domain's real service-area count, set inline
   by home.mjs from data/services.mjs — not an invented utilisation bar. */
.stack-row-meter-fill {
  display: block; height: 100%; border-radius: inherit;
  background: linear-gradient(90deg, var(--accent-1), var(--signal-ok));
  opacity: 0.75;
}
.stack-row-lamp {
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--signal-ok);
  box-shadow: 0 0 0 3px rgba(var(--signal-ok-rgb),0.16);
}

/* .hero-precision-act{--arm} + .stack-row-armed* (the Act 1 -> Act 2 handoff
   that used to trip the stack-instrument's bottom band from nominal to
   alert as the hero's pin ran out) removed 2026-08-26 along with
   stackInstrument() itself — see home.mjs. No markup references either
   class anymore. */
@media (hover: hover) and (pointer: fine) {
  .stack-row:hover { background: rgba(var(--hero-ink-rgb),0.06); }
}
.stack-row:focus-visible { outline: 2px solid var(--accent-1); outline-offset: 2px; }

.stack-instrument-foot {
  display: flex; align-items: center; gap: 8px;
  margin: 8px 6px 2px;
  padding-top: 11px;
  border-top: 1px solid rgba(var(--hero-ink-rgb),0.10);
  font-family: var(--font-mono); font-size: 10.5px; letter-spacing: 0.08em; text-transform: uppercase;
  color: rgba(var(--hero-ink-rgb),0.68); /* was 0.55 — measured 4.25:1 in light */
}
.stack-instrument-foot-dot {
  width: 5px; height: 5px; border-radius: 50%; background: var(--accent-1); flex: none;
}

.hero-precision-scroll-cue {
  position: absolute; left: 50%; bottom: 34px; transform: translateX(-50%);
  z-index: 1;
  display: flex; align-items: center; gap: 12px;
  font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.14em; text-transform: uppercase;
  /* impeccable audit finding (2026-08-24): 0.4 alpha measured 3.39:1 against
     --hero-canvas, below WCAG AA's 4.5:1 for normal text. 0.62 clears it in
     both themes while staying visually secondary. */
  color: rgba(var(--hero-ink-rgb),0.62);
  white-space: nowrap;
}
.hero-precision-scroll-cue-rule {
  display: block; width: 46px; height: 1px;
  background: linear-gradient(90deg, transparent, rgba(var(--hero-ink-rgb),0.5));
}
@media (max-width: 900px) { .hero-precision-scroll-cue { display: none; } }

@media (max-width: 1023px) {
  .hero-precision-layout { gap: 36px; padding-top: calc(var(--nav-h) + 16px); }
  .hero-precision-h1 { font-size: clamp(34px, 8vw, 52px); max-width: 20ch; }
  /* Tagline variant keeps its own full-width/smaller-size rule at this
     breakpoint too — same specificity as the line above, and without this
     the line above (later in the cascade) would win and re-clip these
     3 full-phrase lines back down to a 20ch column meant for the old
     short-word auto-wrap headline. */
  .hero-precision-h1--tagline { max-width: 100%; font-size: clamp(28px, 5.4vw, 44px); }
  .hero-precision-sub { font-size: 16px; margin-bottom: 26px; }
}
/* Below this the stage cannot hold both a full headline and the instrument at
   its desktop density inside 100svh. The previous answer was to hide the
   instrument's head, its foot AND two of its five rows — which left a phone
   reader looking at a headline that says "Your estate has FIVE layers" beside
   an unlabelled panel listing three (2026-08-24 #3).
   Compress the rows instead of deleting them: the meter bar is the one part
   of a row that is decorative rather than informational at this width, and
   dropping it takes a row from ~48px to ~34px — enough for all five plus the
   head on any normal phone. */
@media (max-width: 560px) {
  .hero-precision-layout { gap: 22px; }
  .hero-precision-sub { margin-bottom: 20px; }
  .hero-precision-cta { gap: 12px; }
  .hero-precision-cta .btn { width: 100%; justify-content: center; }
}
/* One line, not two. The domain strip wrapped to two lines on every phone,
   which cost ~40px of the tightest stage on the site to say something the
   headline says better. */
@media (max-width: 560px) {
  .hero-precision-eyebrow {
    font-size: 9.5px; letter-spacing: 0.1em; padding: 7px 12px; gap: 8px;
    margin-bottom: 16px;
  }
}

/* ===== Why Trionova — the split comparison (2026-08-24 #2) ================
   Replaces the old .why-grid / .chaos-diagram / .hub-diagram pair. Built from
   .stack-instrument and .stack-row so the shape a visitor learned in the hero
   is the shape that carries the argument here. See home.mjs for why the
   glossy-icon diagrams had to go.
   ========================================================================= */
.why-headline { max-width: 24ch; }
/* 2026-08-27, client-directed: new copy for this section's intro, styled
   "bold and suit the website UI" against a reference screenshot (another
   vendor's site — a large bold statement paragraph with one phrase picked
   out in a solid-fill highlight pill). Borrows that RECIPE — bold lede
   paragraph, one inline highlighted phrase — but in Trionova's own blue
   accent (not the reference's green) and at a weight/size that sits under
   .why-headline as a supporting statement, not a second competing
   headline. */
.why-lede { font-family: var(--font-display); font-weight: 600; font-size: clamp(19px, 2.1vw, 27px); line-height: 1.5; color: var(--ink); max-width: 58ch; }
.why-highlight { display: inline; background: var(--accent-1); color: #fff; padding: 2px 10px; border-radius: 8px; box-decoration-break: clone; -webkit-box-decoration-break: clone; }

/* 2026-08-24 #11 — the two-column "five vendors / one team" split is now one
   diagonal reveal instead of a side-by-side comparison. No-JS default: an
   ordinary stacked column (each layer full width, one above the other) —
   the content exists and reads fine with zero script. `.js` scopes the CSS
   grid stacking (both layers in the same cell) that the wipe needs, so a
   visitor without JS never gets an unreadable double-exposure; they get the
   plain stacked version, same as everyone gets below 1024px anyway. */
.why-pin { position: relative; }
.why-stage { position: relative; display: flex; flex-direction: column; gap: 32px; --why-p: 0; }
.why-layer { min-width: 0; }
@media (min-width: 1024px) {
  /* 2026-08-24 #17 — reserved scroll budget for the hold-and-scrub, gated
     to >=1024px and `.js` for the same reason the wipe itself is: below
     that width (or without script) there's nothing here to hold a budget
     open for, .why-stage stays the plain stacked column. 2.2 viewport-
     heights of container against a stage that's ~1 viewport tall leaves
     ~1.2 viewport-heights of ACTUAL held scroll — enough to feel
     deliberate without overstaying it. */
  .js .why-pin { height: 220vh; }
  .js .why-stage {
    display: grid; align-items: start; align-content: center;
    position: sticky; top: 0;
    min-height: 100svh;
    padding-top: calc(var(--nav-h) + var(--nav-top) + 24px);
    box-sizing: border-box;
  }
  .js .why-layer { grid-area: 1 / 1; }
  .js .why-layer-good {
    /* --surface is a deliberately near-transparent tint (2-3% overlay) —
       right for an ordinary single-layer page, wrong here: without an
       opaque backing of its own, this layer doesn't actually OCCLUDE the
       "bad" layer underneath within the revealed wedge, it just tints it —
       confirmed live, the frag-cards were ghosting through the reveal.
       var(--bg-alt) matches the section's own ground exactly, so the panel
       reads as solid without introducing a visible card edge of its own. */
    background: var(--bg-alt);
    border-radius: 20px;
    /* Layer promotion so the browser isn't caught building one mid-scroll —
       animating clip-path on a panel full of text/icons is expensive to
       repaint on the main thread otherwise, which is what "not smooth" was. */
    will-change: clip-path;
    backface-visibility: hidden;
    transform: translateZ(0);
    /* Depth (soft, dark) + a crisp accent-coloured rim with zero blur.
       filter:drop-shadow, unlike box-shadow, traces the element's actual
       CLIPPED silhouette, so that rim genuinely sweeps along the diagonal
       cut as it grows rather than sitting around the panel's rectangle. */
    filter:
      drop-shadow(0 22px 48px rgba(0,0,0,0.16))
      drop-shadow(-4px 3px 0 rgba(var(--accent-1-rgb),0.55));
    /* 2026-08-24 #14 — was a timed CSS transition fired once by an
       IntersectionObserver threshold: play a fixed-length animation, done,
       regardless of how the visitor kept scrolling. Structurally incapable
       of being "visible while scrolling slowly" — once triggered it's
       decoupled from scroll input entirely. Rebuilt as a straight scrub:
       --why-p (0-1) is set every scroll frame by why-reveal.js off the
       stage's own position in the viewport, and this clip-path is a pure
       function of that one number — no transition, no timer, no class
       toggle. The wedge is exactly as far open as the page is scrolled,
       in both directions, at whatever speed the visitor is scrolling at.
       Same 4-point "everything right of this diagonal line" shape as
       before, just driven by calc() instead of interpolated between two
       fixed end states. */
    clip-path: polygon(
      calc(196% - 196% * var(--why-p)) 0,
      100% 0,
      100% 100%,
      calc(100% - 196% * var(--why-p)) 100%
    );
  }
}
.split-label {
  display: inline-flex; align-items: center; gap: 9px;
  font-family: var(--font-mono); font-size: 10.5px; letter-spacing: 0.2em; text-transform: uppercase;
  margin-bottom: 14px;
}
.split-label-dot { width: 6px; height: 6px; border-radius: 50%; flex: none; }
.split-label-bad { color: var(--signal-alert); }
.split-label-bad .split-label-dot { background: var(--signal-alert); box-shadow: 0 0 0 3px rgba(var(--signal-alert-rgb),0.18); }
.split-label-good { color: var(--signal-ok); }
.split-label-good .split-label-dot { background: var(--signal-ok); box-shadow: 0 0 0 3px rgba(var(--signal-ok-rgb),0.18); }
.split-title {
  font-size: clamp(19px, 1.9vw, 25px); font-weight: 600; line-height: 1.32;
  letter-spacing: -0.01em; color: var(--ink);
  max-width: 26ch; margin-bottom: 26px;
}

/* 2026-08-24 #15 — client: the "before" side needed to visibly LOOK like a
   problem (it read as five neutral grey cards with one small red one — the
   copy argued urgency, the colour didn't), and the two layers' row rhythm
   didn't line up, so the diagonal wipe crossed mismatched text. Fixed both
   at once by giving the fragmented side the same bordered "instrument"
   frame — header bar + rows — the unified side already had: same shape
   means the two layers' rows now share the same vertical rhythm (the
   mismatch was the stack panel's own header bar height not being answered
   on the other side), and it lets every card carry the red treatment
   instead of just the one actual fault, which is the more honest picture
   anyway — from outside, a vendor's board reporting "fine" when the
   incident is actually elsewhere is no more trustworthy than the one
   visibly alarmed. */
.frag-panel {
  border: 1px solid rgba(var(--signal-alert-rgb),0.22);
  border-radius: 14px;
  background: var(--surface);
  box-shadow: 0 0 0 1px rgba(var(--signal-alert-rgb),0.08), 0 20px 48px rgba(var(--signal-alert-rgb),0.07);
  overflow: hidden;
}
.frag-panel-head {
  display: flex; align-items: baseline; justify-content: space-between;
  padding: 11px 15px;
  border-bottom: 1px solid rgba(var(--signal-alert-rgb),0.16);
  font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.2em; text-transform: uppercase;
}
.frag-panel-title { color: var(--signal-alert); font-weight: 600; }
.frag-panel-sub { color: var(--muted); }
.frag-grid { display: flex; flex-direction: column; gap: 8px; padding: 10px; }
.frag-card {
  display: grid; grid-template-columns: auto minmax(0,1fr) auto;
  align-items: center; gap: 14px;
  padding: 14px 14px;
  border: 1px dashed rgba(var(--signal-alert-rgb),0.3);
  border-radius: 10px;
  /* Every card carries the red tint now, not just the actual fault (see the
     comment above the panel) — the fault card stands out by being solid
     rather than tinted, and by its lamp, not by being the only red thing
     on the board. */
  background: rgba(var(--signal-alert-rgb),0.05);
  box-shadow: inset 0 1px 0 var(--fill-2);
}
.frag-card-vendor {
  font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--muted);
}
.frag-card-label { font-size: 14px; font-weight: 600; color: var(--ink); }
.frag-card-lamp {
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--signal-alert); opacity: 0.45;
}
.frag-card-alert {
  border: 1px solid var(--signal-alert);
  background: rgba(var(--signal-alert-rgb),0.12);
}
.frag-card-alert .frag-card-lamp {
  background: var(--signal-alert); opacity: 1;
  box-shadow: 0 0 0 4px rgba(var(--signal-alert-rgb),0.24);
}
.frag-card-alert .frag-card-label { color: var(--signal-alert); }
.frag-caption {
  display: flex; align-items: flex-start; gap: 10px;
  margin-top: 20px;
  font-size: 14.5px; line-height: 1.5; color: var(--muted);
}
.frag-caption-mark {
  flex: none; width: 20px; height: 20px; border-radius: 50%;
  display: grid; place-items: center;
  background: rgba(var(--signal-alert-rgb),0.14); color: var(--signal-alert);
  font-family: var(--font-mono); font-size: 12px; font-weight: 700;
  margin-top: 1px;
}
/* The "after" side's payoff line — same shape as .frag-caption, green. */
.split-caption-good {
  display: flex; align-items: flex-start; gap: 10px;
  margin-top: 20px;
  font-size: 14.5px; line-height: 1.5; color: var(--muted);
}
.split-caption-mark-good {
  flex: none; width: 22px; height: 22px; border-radius: 50%;
  display: grid; place-items: center;
  background: rgba(var(--signal-ok-rgb),0.16); color: var(--signal-ok);
  font-family: var(--font-mono); font-size: 13px; font-weight: 700;
  margin-top: 1px;
}

/* The unified side reuses the hero instrument, minus the glass/blur (it sits
   on an ordinary section ground here, not over the world canvas). Green is
   pushed further than the red side's tint deliberately asymmetric: the
   "after" state is the resolved one, so it gets to look calm and solid
   rather than urgent — happiness reads as clean and settled, not loud. */
.stack-instrument-flat {
  --hero-ink-rgb: var(--ink-rgb, 16,20,26);
  background: var(--surface);
  border: 1px solid rgba(var(--signal-ok-rgb),0.3);
  backdrop-filter: none; -webkit-backdrop-filter: none;
  box-shadow: 0 0 0 1px rgba(var(--signal-ok-rgb),0.14), 0 20px 48px rgba(var(--signal-ok-rgb),0.14);
}
.stack-instrument-flat .stack-row {
  background: rgba(var(--signal-ok-rgb),0.05);
  border-radius: 8px;
  margin: 3px 6px;
}
.stack-instrument-flat .stack-row-label { color: var(--ink); }
.stack-instrument-flat .stack-row-num,
.stack-instrument-flat .stack-instrument-sub { color: var(--muted); }
.stack-instrument-flat .stack-instrument-title { color: var(--signal-ok); font-weight: 600; }
.stack-instrument-flat .stack-row + .stack-row { border-top: none; }
.stack-instrument-flat .stack-instrument-head { border-color: rgba(var(--signal-ok-rgb),0.18); }
.stack-instrument-flat .stack-instrument-foot { border-color: var(--border-dark); color: var(--muted); }
.stack-instrument-flat .stack-row-lamp {
  background: var(--signal-ok);
  box-shadow: 0 0 0 4px rgba(var(--signal-ok-rgb),0.22);
}

.split-divider {
  align-self: stretch;
  display: flex; align-items: center; justify-content: center;
  width: 1px;
  background: linear-gradient(180deg, transparent, var(--border-dark) 18%, var(--border-dark) 82%, transparent);
}
.split-divider-arrow {
  display: grid; place-items: center;
  width: 34px; height: 34px; border-radius: 50%;
  /* was var(--bg-alt) — identical to this section's own background in dark
     theme, so the marker disappeared into the ground it's meant to sit on
     top of. --surface gives it the same "floating chip" elevation as every
     other circular badge on the page. */
  background: var(--surface); border: 1px solid var(--border-dark);
  box-shadow: 0 8px 20px rgba(0,0,0,0.12);
  color: var(--accent-1);
}
.why-checklist {
  display: flex; flex-wrap: wrap; gap: 10px;
  list-style: none; margin-top: 22px;
}
/* Tinted green chips, not bare bordered pills — same chip language the
   incident trace legend and log tags already use elsewhere on the page,
   so the "after" side reads as settled/positive at a glance, not just via
   the small leading icon. */
.why-checklist li {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 8px 14px;
  border: 1px solid rgba(var(--signal-ok-rgb),0.28);
  background: rgba(var(--signal-ok-rgb),0.08);
  border-radius: 999px;
  font-size: 13.5px; font-weight: 500; color: var(--ink);
}
.why-checklist i, .why-checklist svg { color: var(--signal-ok); flex: none; }

/* Below 1024: .why-stage is already a plain flex column (its base rule),
   so the diagonal wipe simply never activates and both layers read as an
   ordinary stacked "before, then after" — no separate override needed. */
@media (max-width: 1023px) {
  .split-title { max-width: none; }
}

/* ----- Hero headline entrance --------------------------------------------
   Motion QA finding (2026-08-24 #2), and a real one: the H1 carries
   data-sc-kinetic="lines", but its cue is "0 0.78 0" — rIn 0, which is
   REQUIRED so the hero greets instantly at p=0 (see the long note in
   home.mjs). The side effect is that the engine's per-unit stagger resolves
   to opacity 1 for every line on the very first frame, so the largest element
   on the page went 0 -> 1 in a single frame. Not a flicker; simply no
   entrance at all, on the one element that most deserves one.

   Fixed in CSS on the OUTER .sc-split--line wrapper, which the engine never
   touches — it rewrites inline styles on .sc-split__i every read(), so
   animating the inner unit would be overwritten 60x/second. The animation is
   gated on .sc-ready (added by the engine once it has laid out and split the
   text), so it starts when the lines actually exist, and it runs once rather
   than being scroll-linked: this is a page-load entrance, not a scrub. */
@media (prefers-reduced-motion: no-preference) {
  .sc-ready .hero-precision-h1 .sc-split--line {
    animation: hero-line-in 700ms cubic-bezier(0.23, 1, 0.32, 1) both;
  }
  .sc-ready .hero-precision-h1 .sc-split--line:nth-child(1) { animation-delay: 120ms; }
  .sc-ready .hero-precision-h1 .sc-split--line:nth-child(2) { animation-delay: 210ms; }
  .sc-ready .hero-precision-h1 .sc-split--line:nth-child(3) { animation-delay: 300ms; }
  .sc-ready .hero-precision-h1 .sc-split--line:nth-child(n+4) { animation-delay: 390ms; }
  /* Tagline variant (2026-08-26): 3 separate kinetic elements instead of one
     auto-split h1, each producing exactly one .sc-split--line child of its
     own — so the sibling-based nth-child rules above only ever match
     :nth-child(1) for all three (each is an only child) and they'd all fire
     at the same 120ms. Same selector shape, same specificity, scoped to the
     line wrapper's OWN position instead of its single child's — placed after
     the rules above so it wins ties in source order without needing
     !important. */
  .sc-ready .hero-precision-h1-line:nth-child(1) .sc-split--line { animation-delay: 120ms; }
  .sc-ready .hero-precision-h1-line:nth-child(2) .sc-split--line { animation-delay: 210ms; }
  .sc-ready .hero-precision-h1-line:nth-child(3) .sc-split--line { animation-delay: 300ms; }
}
@keyframes hero-line-in {
  from { opacity: 0; transform: translateY(0.34em); }
  to   { opacity: 1; transform: none; }
}

/* ----- Tier-2 WebGL layer ------------------------------------------------
   The host is always in the DOM; the <canvas> inside it only exists once
   webgl-scene.js has cleared its reduced-motion / WebGL-support / setup
   gates AND painted a first frame. `webgl-live` on <html> is what makes the
   three world sections transparent so the canvas shows through — which means
   the CSS/SVG hero is the DEFAULT and the 3D is the enhancement, never the
   other way round. If the class never lands, nothing below applies and the
   page is exactly what it was before this layer existed.

   z-index -1 puts the canvas above the body background and below every
   in-flow section, so the opaque sections after the world simply cover it
   with no stacking-context gymnastics and no per-section z-index. */
.world-canvas-host {
  position: fixed; inset: 0; z-index: -1;
  pointer-events: none;
  opacity: 0;
  transition: opacity 700ms cubic-bezier(0.23, 1, 0.32, 1);
}
/* Faded in on arrival rather than snapped: the scene's first frame lands
   whenever three.js finishes parsing, which is not a moment tied to anything
   the visitor did, and a lattice that pops into existence draws attention to
   the machinery instead of the page. */
.world-canvas-host.is-active { opacity: 1; }
.world-canvas { display: block; width: 100%; height: 100%; }

/* Hand the ground to the canvas only when it is genuinely live. The renderer
   clears to transparent and the host sits over the body background, so the
   sections must stop painting --hero-canvas or they would cover the lattice
   entirely. */
.webgl-live .hero-precision,
.webgl-live .hero-precision-silence { background: transparent; }
/* The body has to carry the world's ground instead for the duration, or the
   transparent acts would show the page background (--bg) rather than the
   world's (--hero-canvas). A fixed layer, not a body-background swap, so the
   ordinary sections further down are unaffected. */
.webgl-live .world-canvas-host::before {
  content: ''; position: absolute; inset: 0; z-index: -1;
  background: var(--hero-canvas);
}
/* The generated backdrop is still wanted on top of the lattice, but at lower
   weight — two competing depth cues at full strength read as clutter. */
.webgl-live .world-backdrop { opacity: 0.07; }
[data-theme="dark"] .webgl-live .world-backdrop,
.webgl-live[data-theme="dark"] .world-backdrop { opacity: 0.34; }
/* Likewise the flat CSS grid: with a real lattice behind it, the 2D grid
   becomes a moiré partner rather than structure. Keep a trace of it so the
   drawing language survives, drop most of its weight. */
.webgl-live .hero-precision-grid { opacity: 0.35; }

/* Legibility over the lattice. The field is deliberately dense, which is
   right behind a headline and wrong behind an 11px mono log and a 1.5px
   trace stroke. Two targeted corrections rather than dimming the whole scene:
   a soft ground behind the copy column only, and a canvas-coloured glow on
   the trace strokes so they separate from whatever is behind them. Both are
   scoped to .webgl-live, so the non-WebGL page is untouched. */
.webgl-live .incident-frame::before {
  content: ''; position: absolute; inset: -3% -2%; z-index: -1; pointer-events: none;
  background: radial-gradient(ellipse 58% 66% at 28% 52%, rgba(var(--hero-canvas-rgb),0.86), rgba(var(--hero-canvas-rgb),0) 72%);
}
.webgl-live .incident-trace-path,
.webgl-live .incident-trace-rules line {
  filter: drop-shadow(0 0 7px rgba(var(--hero-canvas-rgb),0.95));
}
.webgl-live .hero-precision-copy {
  /* Same idea on the hero, weaker: the headline is large enough to hold its
     own, the sub-paragraph is not. */
  position: relative;
}
.webgl-live .hero-precision-copy::before {
  content: ''; position: absolute; inset: -8% -6% -10% -8%; z-index: -1; pointer-events: none;
  background: radial-gradient(ellipse 62% 60% at 34% 50%, rgba(var(--hero-canvas-rgb),0.7), rgba(var(--hero-canvas-rgb),0) 74%);
}
/* The silence beat is the last section over the canvas; below it an opaque
   section simply covers the fixed layer, which lands as a hard horizontal
   seam mid-lattice. Fade the world out into the next section's ground so the
   world ENDS rather than being cut off. */
.webgl-live .hero-precision-silence::after {
  content: ''; position: absolute; inset: auto 0 0 0; height: 42vh; z-index: 1; pointer-events: none;
  background: linear-gradient(180deg, rgba(var(--hero-canvas-rgb),0), var(--bg-alt));
}

/* ----- Skip affordance ---------------------------------------------------- */
/* Hidden state is the CSS DEFAULT on the element, not a class added by script
   — so there is no frame in which it paints visible and is then hidden. That
   is the same failure mode the project's GSAP FOUC rule exists to prevent,
   and it applies to any pre-hidden element, GSAP-driven or not. main.js only
   ever ADDS .is-available, never removes a hiding class. */
.skip-acts {
  position: fixed; right: 22px; bottom: 22px; z-index: 54;
  display: inline-flex; align-items: center; gap: 9px;
  padding: 9px 14px;
  border-radius: 999px;
  border: 1px solid rgba(var(--hero-ink-rgb),0.16);
  background: rgba(var(--hero-canvas-rgb),0.7);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  font-family: var(--font-mono); font-size: 10.5px; letter-spacing: 0.12em; text-transform: uppercase;
  color: rgba(var(--hero-ink-rgb),0.68);
  opacity: 0;
  pointer-events: none;
  transform: translateY(6px);
  transition: opacity 420ms var(--sc-ease-out, cubic-bezier(0.23,1,0.32,1)),
              transform 420ms var(--sc-ease-out, cubic-bezier(0.23,1,0.32,1)),
              color 0.2s ease, border-color 0.2s ease;
}
.skip-acts.is-available { opacity: 1; pointer-events: auto; transform: none; }
/* Keyboard users get it immediately on focus regardless of dwell — a control
   that only exists after a scroll gesture is not reachable by tab at all. */
.skip-acts:focus-visible {
  opacity: 1; pointer-events: auto; transform: none;
  outline: 2px solid var(--accent-1); outline-offset: 3px;
}
.skip-acts:hover { color: var(--hero-ink); border-color: rgba(var(--hero-ink-rgb),0.34); }
.skip-acts-icon { display: inline-flex; opacity: 0.7; }
/* Once past the pinned acts it has nothing to skip to; main.js drops
   .is-available and this returns to its resting hidden state. */
@media (max-width: 560px) {
  /* Bottom-right at this width is the WhatsApp floater's corner. Move to the
     top edge under the nav rather than stacking two round controls. */
  .skip-acts { right: 14px; bottom: auto; top: calc(var(--nav-h) + var(--nav-top) + 12px); padding: 7px 11px; font-size: 10px; }
}
/* Desktop: the bottom-right corner already holds the WhatsApp floater and the
   scroll-to-top button. Sit clear of both rather than overlapping them —
   confirmed as a real collision in a 1440px screenshot, not assumed. */
@media (min-width: 561px) {
  .skip-acts { right: calc(22px + 78px); }
}

/* ----- Act 2: the incident (the peak) ------------------------------------- */
.incident-frame {
  position: relative; z-index: 1;
  width: 100%;
  max-width: 1240px;
  margin-inline: auto;
  padding-inline: 24px;
  /* 2026-08-24 #9 — was nav-h + 46px. This top clearance is the other
     (fixed, span-independent) component of the hero->incident "blank gap"
     measured in the same pass that cut HERO_SPAN above: the badge sits this
     far below the top of the incident stage no matter how tall the hero's
     own pin budget is. Trimmed to the same nav-h + 12px the ≤640px rule
     below already uses — plenty of clearance under the floating nav, less
     dead space before the badge is the first thing on screen. */
  padding-top: calc(var(--nav-h) + 12px);
  padding-bottom: 78px;
  display: flex; flex-direction: column;
  justify-content: center;
  gap: clamp(16px, 3vh, 34px);
  height: 100%;
}
.incident-badge {
  align-self: center;
  display: inline-flex; align-items: center; gap: 9px;
  font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--signal-amber);
  border: 1px solid rgba(var(--signal-amber-rgb),0.34); background: rgba(var(--signal-amber-rgb),0.08);
  border-radius: 999px; padding: 7px 15px;
  max-width: 100%;
  text-align: center;
}
.incident-badge-dot {
  width: 5px; height: 5px; border-radius: 50%; background: var(--signal-amber); flex: none;
}
@media (max-width: 480px) {
  /* Confirmed real at 390px in the first pass's review, and the previous fix
     (white-space:normal + a squarer radius) only stopped the overflow — the
     pill still ran nearly edge to edge. Shorten what it has to hold instead:
     the dot plus a two-line label inside a real box. */
  .incident-badge { border-radius: 12px; line-height: 1.45; padding: 8px 12px; font-size: 9.5px; letter-spacing: 0.1em; align-items: flex-start; }
  .incident-badge-dot { margin-top: 5px; }
}

.incident-grid {
  display: grid;
  grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr);
  gap: clamp(24px, 4vw, 56px);
  align-items: start;
}
.incident-copy {
  position: relative;
  width: 100%;
  min-height: clamp(190px, 26vh, 280px);
  /* CRITICAL FIX (2026-08-24): the copy was completely invisible over the WebGL
     lattice. Adding a real ground behind the text so it's always readable. */
  padding: clamp(20px, 3vh, 32px);
  background: rgba(var(--incident-card-rgb, 5,7,10), 0.90);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border-radius: 16px;
  /* The card has to READ as a card in light theme too (2026-08-24 #3). A
     near-white ground with a 0.1-alpha dark hairline over a pale lattice is
     invisible: the copy looked like text floating on noise rather than text
     on a panel, which is half of why the light-theme act read as "low
     visibility". Give it a real edge and a real shadow — dark theme keeps a
     glow-free hairline since it separates on value alone. */
  border: 1px solid rgba(var(--hero-ink-rgb),0.12);
  box-shadow: 0 18px 44px rgba(var(--hero-ink-rgb),0.10);
  /* The five cues stack in ONE grid cell rather than being absolutely
     positioned (2026-08-24 #3). Absolute positioning took every cue out of
     flow, so the card's height came only from the guessed `min-height` above
     — and once the cue body scaled up to clamp(26px,3.2vw,42px) the longest
     cue ("Pool limits raised, replica reconfigured…") ran four lines and
     spilled straight out of the card, over the trace SVG below it, with no
     ground behind it. Confirmed live at 1440x900, dark theme. Grid-stacking
     keeps the "only one visible at a time" behaviour (the engine still drives
     opacity per cue) while letting the card size itself to the TALLEST cue,
     so it fits every cue and never reflows between them. */
  display: grid;
  align-items: start;
}
.incident-cue {
  grid-area: 1 / 1;
  max-width: 34ch;
}
.incident-cue-mono {
  font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--signal-amber);
  margin-bottom: 12px;
}
.incident-cue-body {
  font-family: var(--font-display); font-weight: 600;
  /* Sized against the viewport HEIGHT as well as its width: this lives inside
     a 100svh pinned stage next to the console, so a width-only clamp blows the
     card out on short laptop viewports (1440x900 was the failing case). */
  font-size: clamp(24px, min(3.1vw, 4svh), 40px);
  line-height: 1.32; /* increased from 1.28 */
  letter-spacing: -0.015em;
  color: var(--hero-ink);
  max-width: 20ch; /* was 22ch */
}

/* The console. A real ordered list, so it reads as a sequence to a screen
   reader too, not just as decoration. */
.incident-console {
  /* Was rgba(--hero-ink-rgb, 0.035) — 3.5% opacity, effectively still
     transparent glass over the busy WebGL lattice. .incident-copy (the card
     to its left) got a "real ground" fix for this exact problem (see its own
     comment above) but this panel was missed, so it kept the readability bug
     the fix was written to solve. Now matches .incident-copy's proven
     treatment: same solid backing, blur, border and shadow. */
  border: 1px solid rgba(var(--hero-ink-rgb),0.12);
  border-radius: 14px;
  background: rgba(var(--incident-card-rgb, 5,7,10), 0.90);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  box-shadow: 0 18px 44px rgba(var(--hero-ink-rgb),0.10);
  overflow: hidden;
}
.incident-console-head {
  display: flex; align-items: center; gap: 8px;
  padding: 11px 15px;
  border-bottom: 1px solid rgba(var(--hero-ink-rgb),0.1);
  font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.2em; text-transform: uppercase;
  color: rgba(var(--hero-ink-rgb),0.6);
}
.incident-console-dot {
  width: 6px; height: 6px; border-radius: 50%; background: var(--signal-amber);
}
.incident-console-body { padding: 10px 8px; margin: 0; list-style: none; }
/* 2026-08-24 #7 — client feedback: improve the log's UI. Was a flat list of
   plain-colour tag text + message with no rhythm between rows. Now a real
   timeline: each row gets a rail dot (hollow until reached, filled + glowing
   once live) on a connecting hairline, the tag becomes a coloured pill
   instead of bare text (same chip language the trace legend now uses, so
   the two live instruments in this act read as one system), and the single
   newest live row — the "cursor", found with :has() rather than tracked in
   JS — gets a soft tinted background so the log reads as something actively
   arriving, not a static transcript. */
.incident-console-line {
  position: relative;
  display: grid; grid-template-columns: auto 4.9em minmax(0,1fr); gap: 10px; align-items: center;
  padding: 7px 10px 7px 4px;
  font-family: var(--font-mono); font-size: 11.5px; line-height: 1.45;
  color: rgba(var(--hero-ink-rgb),0.85);
  border-radius: 8px;
  /* Resting state: present in the DOM, dim, not yet reached. Not
     display:none — a log that pops into existence reflows the panel under
     the reader's eye on every line. 0.16 was low enough that the un-reached
     lines read as a half-broken render rather than a queued log, especially
     in light theme where they all but vanish; 0.3 still puts them clearly
     behind the live lines but keeps them legible AS structure. */
  opacity: 0.3;
  transition: opacity 300ms ease, color 300ms ease, background 300ms ease;
}
.incident-console-line + .incident-console-line { border-top: 1px solid rgba(var(--hero-ink-rgb),0.06); }
.incident-console-line.is-live { opacity: 1; }
.incident-console-line.is-live:not(:has(~ .is-live)) {
  background: rgba(var(--signal-amber-rgb),0.09);
}
/* The rail dot: hollow ring resting, filled + glowing once its line is live. */
.incident-console-line::before {
  content: ''; width: 6px; height: 6px; border-radius: 50%;
  background: transparent;
  box-shadow: 0 0 0 1.5px rgba(var(--hero-ink-rgb),0.28) inset;
}
.incident-console-line.is-live::before {
  background: var(--signal-ok);
  box-shadow: 0 0 0 1.5px var(--signal-ok), 0 0 6px rgba(var(--signal-ok-rgb),0.7);
}
.incident-console-tag {
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 9px; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase;
  padding: 3px 0;
  border-radius: 5px;
  background: rgba(var(--hero-ink-rgb),0.06);
  color: rgba(var(--hero-ink-rgb),0.55);
}
.is-live .incident-console-tag[data-tag="ack"],
.is-live .incident-console-tag[data-tag="ok"] { background: rgba(var(--signal-ok-rgb),0.14); color: var(--signal-ok); }
.is-live .incident-console-tag[data-tag="monitor"],
.is-live .incident-console-tag[data-tag="page"] { background: rgba(var(--signal-alert-rgb),0.14); color: var(--signal-alert); }
.is-live .incident-console-tag[data-tag="trace"],
.is-live .incident-console-tag[data-tag="diag"],
.is-live .incident-console-tag[data-tag="apply"] { background: rgba(var(--signal-amber-rgb),0.14); color: var(--signal-amber); }
@media (prefers-reduced-motion: reduce) {
  /* Every line simply present, at full strength. A reader who has asked for
     no motion should not have to scroll a pinned act to read a log. */
  .incident-console-line { opacity: 1; transition: none; }
}

/* ----- The trace ---------------------------------------------------------- */
.incident-trace-wrap { position: relative; width: 100%; }
.incident-trace {
  display: block;
  width: 100%;
  height: auto;
  /* Uniform scaling, so height follows width off the 1200:170 viewBox. NO
     max-height here: capping the height of a uniformly-scaled SVG shrinks its
     WIDTH too, which is what left the band floating inset with dead margins
     either side instead of spanning the frame. The stage is given enough room
     for the full band instead. */
}
.incident-trace-rules line {
  stroke: rgba(var(--hero-ink-rgb),0.11);
  stroke-width: 1;
  stroke-dasharray: 3 7;
  vector-effect: non-scaling-stroke;
}
.incident-trace-path {
  fill: none;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-dasharray: 1 1;
  /* --trace-p is the smoothstepped twin of the engine's raw --sc-p, published
     by telemetry.js on the same frame the visuals are drawn from. Driving the
     draw off raw --sc-p made this the one constant-velocity element in an
     otherwise eased scene. */
  stroke-dashoffset: calc(1 - var(--trace-p, 0));
  vector-effect: non-scaling-stroke;
}
.incident-trace-path-app { stroke: var(--accent-1); }
.incident-trace-path-replica {
  stroke: var(--signal-amber);
  filter: drop-shadow(0 0 5px rgba(var(--signal-amber-rgb),0.3));
}
.incident-trace-path-base { stroke: rgba(var(--hero-ink-rgb),0.35); stroke-width: 1.5; }
.incident-trace-node {
  fill: var(--hero-canvas);
  stroke: var(--signal-amber);
  stroke-width: 2;
  vector-effect: non-scaling-stroke;
  /* Lights as the draw reaches THIS node, not when the draw starts. The 0.06
     window is about one node-diameter of travel, so the lamp comes up just as
     the stroke arrives rather than before or well after it. */
  opacity: clamp(0, calc((var(--trace-p, 0) - var(--node-at, 0)) / 0.06), 1);
}
.incident-trace-node-alert { fill: var(--signal-alert); stroke: var(--signal-alert); }
.incident-trace-node-ok { fill: var(--signal-ok); stroke: var(--signal-ok); }
/* 2026-08-24 #7 — client feedback: the legend read as an afterthought (a
   thin 10px/0.62-opacity mono line) under a trace that's otherwise the
   act's centrepiece. Each lane is now a real chip — a filled dot at full
   lane colour, a tinted pill ground in that same colour, a border — so the
   legend reads as three labelled instruments, not a caption. */
.incident-trace-legend {
  display: flex; flex-wrap: wrap; gap: 8px 10px;
  margin-top: 12px;
}
.incident-trace-legend span {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 5px 12px 5px 10px;
  border-radius: 999px;
  background: var(--lane-color-mix, rgba(var(--hero-ink-rgb),0.06));
  border: 1px solid var(--lane-color-mix-border, rgba(var(--hero-ink-rgb),0.12));
  font-family: var(--font-mono); font-size: 11px; font-weight: 600;
  letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--hero-ink);
}
.incident-trace-legend i { display: block; width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
.incident-trace-legend span:has(i[data-lane="app"]) {
  --lane-color-mix: rgba(var(--accent-1-rgb),0.12);
  --lane-color-mix-border: rgba(var(--accent-1-rgb),0.3);
}
.incident-trace-legend i[data-lane="app"] { background: var(--accent-1); box-shadow: 0 0 6px rgba(var(--accent-1-rgb),0.6); }
.incident-trace-legend span:has(i[data-lane="replica"]) {
  --lane-color-mix: rgba(var(--signal-amber-rgb),0.14);
  --lane-color-mix-border: rgba(var(--signal-amber-rgb),0.35);
}
.incident-trace-legend i[data-lane="replica"] { background: var(--signal-amber); box-shadow: 0 0 6px rgba(var(--signal-amber-rgb),0.6); }
.incident-trace-legend span:has(i[data-lane="base"]) {
  --lane-color-mix: rgba(var(--hero-ink-rgb),0.07);
  --lane-color-mix-border: rgba(var(--hero-ink-rgb),0.18);
}
.incident-trace-legend i[data-lane="base"] { background: rgba(var(--hero-ink-rgb),0.5); }

@media (max-width: 1023px) {
  .incident-grid { grid-template-columns: minmax(0,1fr); gap: 22px; }
  .incident-copy { min-height: clamp(150px, 22vh, 210px); }
  .incident-cue, .incident-cue-body { max-width: none; }
  /* The console is the first thing to go when the stage runs out of height —
     the cue copy is the narrative, the log is the texture around it. Keep the
     first six lines so it still reads as a log, drop the rest. */
  .incident-console-line:nth-child(n+7) { display: none; }
  /* Below 1024 the stage is tight enough that the band's natural height is
     worth trading for stage room — a shorter viewBox slice keeps it full
     width while giving back vertical space. */
  .incident-trace { aspect-ratio: 1200 / 120; object-fit: cover; }
}
@media (max-width: 640px) {
  .incident-console { display: none; }
  .incident-trace-legend { display: none; }
  .incident-frame { gap: 18px; padding-top: calc(var(--nav-h) + 12px); }
}

/* ----- Authored silence: the held beat between the peak and Act 3 ---------
   BRIEF.md, verbatim requirement. This is deliberate dead air and must stay
   legible AS deliberate — which is why it carries a mark at all rather than
   being a genuinely empty half-viewport. The P2 cue-gap fix above exists so
   that this is the ONLY held beat in the sequence; an accidental one earlier
   made this read as more of the same sloppiness rather than as pacing. */
.hero-precision-silence {
  position: relative;
  /* Round 1 (2026-08-24 #4): was a flat 52vh with no ceiling — 988px of blank
     scroll on a tall window, reported as a broken/blank page. Capped with
     clamp(). Round 2, same day: client still saw it as "a lot of blank
     space" even capped at 440px — a held beat that's nearly half a viewport
     tall still reads as empty more than it reads as paced. Shrunk further
     and given the mark a slow pulse (below) so the moment reads as an
     active, deliberate instrument rather than dead space even in a static
     screenshot. */
  min-height: clamp(120px, 20vh, 200px);
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 14px;
  background: var(--hero-canvas);
}
.hero-precision-silence-rule {
  width: 1px; height: 34px;
  background: linear-gradient(180deg, transparent, rgba(var(--signal-ok-rgb),0.5), transparent);
}
.hero-precision-silence-mark {
  position: relative; z-index: 1;
  font-family: var(--font-mono); font-size: 12px; letter-spacing: 0.3em; text-transform: uppercase;
  color: var(--signal-ok);
  opacity: 0.7;
}
/* A slow breathing glow behind the mark — reads as "the system is idle, not
   broken," the same distinction the ops-readout's own pulse makes elsewhere. */
.hero-precision-silence-mark::before {
  content: '';
  position: absolute; left: 50%; top: 50%;
  width: 120px; height: 120px;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  background: radial-gradient(circle, rgba(var(--signal-ok-rgb),0.14), transparent 70%);
  animation: silence-pulse 3.6s ease-in-out infinite;
  z-index: -1;
}
@keyframes silence-pulse {
  0%, 100% { opacity: 0.5; transform: translate(-50%, -50%) scale(0.85); }
  50% { opacity: 1; transform: translate(-50%, -50%) scale(1.15); }
}
@media (prefers-reduced-motion: reduce) {
  .hero-precision-silence-mark::before { animation: none; opacity: 0.7; }
}

/* ----- ops readout ("Signature move") removed 2026-08-31, client-directed:
   "Remove the systems nominal in the website, the one with time and ms."
   Was a persistent fixed-position status pill, layout.mjs/every page —
   see telemetry.js's own removal (main.js) for the driving JS. */
@media (prefers-reduced-motion: reduce) {
  .incident-trace-path, .incident-trace-node { transition: none; }
}

/* ----- Theme crossfade ----------------------------------------------------
   Paired with theme-toggle.js's startViewTransition(). Without this the UA
   default is a 250ms cross-fade that also animates every fixed element's
   position; pinning both halves to the same box and easing them together is
   what makes it read as the page changing state rather than two pages
   dissolving. Reduced motion is handled in the JS (it skips the transition
   entirely), not here, because a 0.01ms view transition still flashes. */
@media (prefers-reduced-motion: no-preference) {
  ::view-transition-old(root),
  ::view-transition-new(root) {
    animation-duration: 280ms;
    animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
  }
}

/* ===========================================================================
   SCHEMATIC PANEL — the connective tissue.
   The homepage's world was the whole point of the first pass and also the
   whole limit of it: every other template inherited the nav and the footer
   and nothing in between, so the site read as one showcase page plus 34
   ordinary content pages. This component is the piece that travels: a
   bounded, gridded, mono-labelled instrument panel that can hold generated
   art, a diagram, or a list, and that carries the same rules/lamps/hairlines
   as the hero instrument. Used on About, Services hub, every domain page,
   every sub-service page and Contact.
   =========================================================================== */
.schematic {
  position: relative;
  border: 1px solid var(--border-dark);
  border-radius: 18px;
  overflow: hidden;
  background: var(--bg-alt);
}
.schematic::before {
  content: ''; position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background-image:
    linear-gradient(var(--grid-line) 1px, transparent 1px),
    linear-gradient(90deg, var(--grid-line) 1px, transparent 1px);
  background-size: 40px 40px;
  mask-image: linear-gradient(180deg, #000, transparent 70%);
  -webkit-mask-image: linear-gradient(180deg, #000, transparent 70%);
}
.schematic-media {
  display: block; width: 100%; height: 100%;
  aspect-ratio: 16 / 10;
  object-fit: cover;
}
/* Generated art is rendered on a neutral mid-slate ground so one asset can
   sit on both a near-white and a near-black section. On the light theme it
   still needs lifting off the page or it reads as a dark hole; on dark it
   needs the opposite. Two small, opposite corrections rather than two
   separate image sets — verified per section, not assumed. */
.schematic-media { filter: saturate(0.92) contrast(1.02); }
[data-theme="light"] .schematic-media { filter: saturate(0.86) contrast(0.96) brightness(1.06); }
@media (prefers-color-scheme: light) {
  :root:not([data-theme="dark"]) .schematic-media { filter: saturate(0.86) contrast(0.96) brightness(1.06); }
}
.schematic-caption {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 2;
  display: flex; align-items: center; gap: 9px;
  padding: 12px 16px;
  background: linear-gradient(180deg, transparent, rgba(0,0,0,0.72));
  font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.14em; text-transform: uppercase;
  color: rgba(255,255,255,0.86);
}
.schematic-caption-dot { width: 5px; height: 5px; border-radius: 50%; background: var(--signal-ok); flex: none; }
.schematic-corner {
  position: absolute; z-index: 2; width: 13px; height: 13px;
  border-color: var(--grid-line-strong); border-style: solid; border-width: 0;
  pointer-events: none;
}
.schematic-corner-tl { top: 10px; left: 10px; border-top-width: 1px; border-left-width: 1px; }
.schematic-corner-tr { top: 10px; right: 10px; border-top-width: 1px; border-right-width: 1px; }
.schematic-corner-bl { bottom: 10px; left: 10px; border-bottom-width: 1px; border-left-width: 1px; }
.schematic-corner-br { bottom: 10px; right: 10px; border-bottom-width: 1px; border-right-width: 1px; }

/* ----- Telemetry strip: the world's chrome, reusable on any page ---------- */
.telemetry-strip {
  display: flex; flex-wrap: wrap; align-items: center; gap: 10px 22px;
  padding: 12px 16px;
  border: 1px solid var(--border-dark);
  border-radius: 12px;
  background: var(--fill-1);
  font-family: var(--font-mono); font-size: 10.5px; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--muted);
}
.telemetry-strip span { display: inline-flex; align-items: center; gap: 8px; }
.telemetry-strip b { font-weight: 500; color: var(--ink); letter-spacing: 0.08em; }
.telemetry-strip i {
  width: 6px; height: 6px; border-radius: 50%; background: var(--signal-ok); flex: none;
  box-shadow: 0 0 0 3px rgba(var(--signal-ok-rgb),0.16);
}

/* ----- Quiet button: the third tier, below primary and ghost --------------
   P2 fix from the first pass. The hero's secondary action was a .btn-ghost —
   a 1px outline at 0.22 alpha sitting directly beside a solid-fill primary.
   Measured 1.9:1 border-to-ground in light and 2.3:1 in dark, i.e. an
   affordance you had to already know was there. The honest fix is not a
   heavier outline (two competing outlined-vs-filled buttons of equal weight
   is its own hierarchy problem) but a different KIND of control: a text
   action with a real underline, which is unambiguously interactive at any
   contrast and stops pretending to be the primary's equal.
   Text colour is --hero-ink, not an alpha of it: 4.5:1+ against the canvas
   in both themes, verified in-browser. */
.btn-quiet {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 6px 2px;
  font-size: 15px; font-weight: 500;
  color: var(--hero-ink);
  background: none; border: 0; border-radius: 4px;
  box-shadow: inset 0 -1px 0 rgba(var(--hero-ink-rgb),0.42);
  transition: box-shadow 0.2s ease, color 0.2s ease;
}
.btn-quiet:hover { box-shadow: inset 0 -1px 0 var(--accent-1); color: var(--accent-1); }
/* Home hero override (2026-08-26) — unconditional-dark split hero (see
   .hero-split above), so this secondary CTA needs a white underline/text
   regardless of site theme, same as .btn-quiet's own --hero-ink color
   normally would already do on the OLD hero-precision background — that
   token isn't tuned for the hero's fixed navy ground, hence the explicit
   white here. */
.hero-split-cta-quiet { color: #fff; box-shadow: inset 0 -1px 0 rgba(255,255,255,0.5); }
.hero-split-cta-quiet:hover { color: var(--accent-1); box-shadow: inset 0 -1px 0 var(--accent-1); }
.btn-quiet:focus-visible { outline: 2px solid var(--accent-1); outline-offset: 3px; }

/* ===== About page: photo (2026-08-26 placeholder → 2026-09-02 stock
   photo, client-directed: "add a stock image for now") =====
   A temporary, generic office/consulting photo (self-hosted webp, not a
   hotlink) standing in until the client supplies their own team/office
   photo — swap the <img src> in about/index.html (and aboutPhotoPlaceholder()
   in about.mjs) when that arrives; nothing else about the layout needs to
   change. Same card treatment (rounded, real border, object-fit:cover)
   the placeholder used, so no layout shift from this swap. */
.about-photo {
  display: block; width: 100%; aspect-ratio: 4 / 3; object-fit: cover;
  border-radius: 20px; border: 1px solid var(--border-dark);
}

/* ===== "Our Presence" map (2026-08-26 → 2026-08-27, client-directed
   across three rounds, now shared between About and Contact — see
   components.mjs's presenceMap()) =====
   Round 1 matched a screenshot of techorbitgroup.com/about's office-city
   map (dashed leader line + pulsing dot + flag label) on this site's own
   schematic/grid ground. Round 2 added a real map backdrop
   (india-map-presence.webp, build-time AI-generated, same pipeline/
   verification standard as every other generated asset, scoped to India
   itself after an earlier full-world attempt proved geographically
   unreliable there — see build-log.md). Round 3, client: the section
   label was unreadable, and the map was visibly CROPPED. Both were the
   same class of bug: components built for the site's normal light-theme
   tokens, dropped into a section that's unconditional-dark like the home
   hero — .section-label was picking up `[data-theme="light"] .section-label`
   (dark text, meant for light backgrounds) since this section never had a
   `.section-dark` escape hatch of its own tokens to fall back on (those
   tokens no longer exist post dark-edition-removal — see main.css's
   retired-dark-edition comment). And the panel forced the image into a
   wide flat bar via background-size:cover, which crops a portrait
   (900×1100) image hard on the sides. Fixed by (1) hardcoding this
   section's label colors explicitly, same pattern as the hero's own
   unconditional-dark overrides, and (2) sizing the map panel to the
   image's OWN aspect-ratio (900/1100) instead of forcing a wide bar, so
   the full island-to-Himalaya shape renders with zero cropping at any
   viewport. Pin percentages below are plain fractions of the now-
   uncropped image (verified with a python3/Pillow test-marker overlay on
   the actual file) — no cover-crop math needed since the panel's aspect
   now matches the source exactly. */
.presence-section { background: #0A0E1A; }
.presence-section .section-label { color: #7DA6FF; }
.presence-section .section-label-num { color: rgba(255,255,255,0.55); }
.presence-section .section-label-rule { background: rgba(255,255,255,0.16); }
/* Client-directed (Changes.docx, 2026-08-30): "change this to world map" —
   swapped the portrait India-shaped map for a wide world map
   (world-map-presence.webp, generated + cropped to its landmass band, see
   assets.config.json), so the panel's own aspect-ratio moved from the old
   900:1100 portrait to match that crop instead.
   2026-08-31 follow-up (client-reported, real): the plain AI-generated
   world map drew India as a barely-there sliver mislabeled/misshapen badly
   enough that the Pune/Bangalore pins landed in open ocean, not on land —
   confirmed by sampling the generated file's own pixels at the pin
   coordinates (ocean color, not land). FLUX-schnell (8-step distilled
   model) proved unable to fix this even with 3 regenerated attempts and a
   much more explicit prompt — small-scale geography at world-map scale is
   past what this model renders reliably. Fixed by compositing the
   already-correct india-map-presence.webp (used for the old India-only
   map, and still geographically accurate on its own) onto the world map
   in place of the broken sliver, recolored to match the world map's own
   land tone so the seam doesn't read as a paste.
   2026-08-31, second follow-up (client-reported, real): the first
   composite pass left a visible rectangle/seam around the pasted India
   (flat erase-fill didn't match this generation's own subtle per-region
   shading), and the two pins sat close enough together that Bengaluru's
   flag box covered Pune's own dot, reading as only one location marked.
   Regenerated the world base again and redid the composite with a
   locally-blurred erase (blur the region into itself with a feathered
   mask, so the fill is always drawn from the image's own surrounding
   tone — genuinely seamless by construction, not a color guess) before
   pasting India back on top; picked new pin coordinates with more
   vertical separation so both dots and both flags render distinctly.
   New crop is 774×509 (~1.52:1) — aspect-ratio updated to match again. */
.presence-map {
  position: relative;
  width: 100%;
  max-width: 640px;
  aspect-ratio: 774 / 509;
  border-radius: 20px;
  overflow: hidden;
  border: 1px solid rgba(255,255,255,0.1);
  background: linear-gradient(155deg, #0A0E1A 0%, #0E1730 100%);
}
.presence-worldmap {
  position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background-image:
    linear-gradient(155deg, rgba(10,14,26,0.15) 0%, rgba(10,14,26,0.4) 100%),
    url("/assets/img/generated/world-map-presence.webp");
  /* Panel's aspect-ratio already matches the source image exactly, so
     cover/contain produce the same result here: the full map, no crop. */
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
}
.presence-grid {
  position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background-image:
    linear-gradient(rgba(125,166,255,0.09) 1px, transparent 1px),
    linear-gradient(90deg, rgba(125,166,255,0.09) 1px, transparent 1px);
  background-size: 36px 36px;
}
/* 2026-08-31, FOURTH pin-layout rework (client-reported, real): once the
   dots were finally corrected to Pune/Bengaluru's true relative geo position
   (see the coordinate comment on each pin below), they landed only ~26px
   apart vertically at typical display size — smaller than a single flag
   box's own height (~30px). Every prior pass tried to fix overlap by tuning
   a *vertical* leader-line length between flag and dot, which is
   mathematically impossible to win: with two dots this close together, no
   line length lets both 30px-tall flags occupy the space between them
   without one flag's box covering the other pin's dot. Fixed by moving to
   L-shaped leaders that run up from each dot, then sideways (Pune's kicks
   left, Bengaluru's kicks right) before the flag — so both flags sit
   clear to either side, in open space, instead of competing for the same
   narrow vertical column above two adjacent points. `.presence-pin` is now
   a zero-size positioning anchor at the true coordinate (its children are
   `position:absolute`, not flex-stacked), so the dot's `left`/`top` inline
   style is exactly the geographic point, independent of however the flag
   leader is routed. */
.presence-pin { position: absolute; z-index: 1; }
.presence-pin-dot {
  position: absolute; left: 0; top: 0; transform: translate(-50%, -50%);
  width: 10px; height: 10px; border-radius: 50%;
  background: var(--accent-1); box-shadow: 0 0 0 4px rgba(62,125,245,0.22);
}
.presence-pin-ping {
  position: absolute; inset: -4px; border-radius: 50%; border: 1.5px solid var(--accent-1);
  animation: hub-ping 2.2s ease-out infinite;
}
/* Leader, vertical stub: rises straight up from the dot. */
.presence-pin-stub-v {
  position: absolute; left: 0; bottom: 0; width: 1px; transform: translateX(-50%);
  background: repeating-linear-gradient(180deg, rgba(125,166,255,0.6) 0 4px, transparent 4px 8px);
}
/* Leader, horizontal stub: kicks sideways (left or right, set per-pin via
   inline `left`/`right`) from the top of the vertical stub to the flag. */
.presence-pin-stub-h {
  position: absolute; height: 1px;
  background: repeating-linear-gradient(90deg, rgba(125,166,255,0.6) 0 4px, transparent 4px 8px);
}
.presence-pin-flag {
  position: absolute;
  background: #fff; color: #0A0E1A;
  font-family: var(--font-mono); font-size: 11px; font-weight: 600; letter-spacing: 0.06em;
  padding: 6px 12px; border-radius: 6px;
  box-shadow: 0 8px 20px rgba(0,0,0,0.35);
  white-space: nowrap;
}
@media (max-width: 640px) {
  .presence-pin-flag { font-size: 10px; padding: 5px 9px; }
}

/* No-JS floor for scrollcraft's devices (2026-08-24 #2). sc-devices.css sets
   [data-sc-cue]{opacity:0} and [data-sc-in]{opacity:0} unconditionally —
   correct while the engine is driving, but on a page where the module fails
   to load (or JS is off entirely) that hides the entire hero and every
   incident cue forever. The site's own [data-reveal] system is gated on .js
   for exactly this reason; this brings the scrollcraft equivalent to the same
   floor. Verified by loading the homepage with JS disabled: hero copy, CTAs
   and all five incident cues render (stacked, unanimated, but present and
   readable) instead of a blank stage.
   Scoped to :not(.js) only, so it costs nothing on the normal path. */
html:not(.js) [data-sc-cue],
html:not(.js) [data-sc-in],
html:not(.js) [data-sc-stagger] > * { opacity: 1 !important; transform: none !important; }
html:not(.js) [data-sc-reveal] { clip-path: none !important; }
/* Without the engine the five incident cues are stacked in one grid cell on
   top of one another. Un-stack them so the sequence reads as a list. */
html:not(.js) .incident-copy { display: block; min-height: 0; }
html:not(.js) .incident-cue { grid-area: auto; margin-bottom: 28px; }
html:not(.js) .incident-cue:last-child { margin-bottom: 0; }

} /* end @layer components (opened line 128) */
