/* generated pseudo-class rules from style-* design attributes */
.p5e9fd60:hover{color:#F2F2F3 !important}
.p50ec701:hover{background:#131316 !important}
.p1c94f10:hover{border-color:#E30613 !important}

/* ===========================================================================
   wirelessmpg.com — build-time layer over the design.
   The generated style-* pseudo-class rules are prepended to this file.

   The design files style everything with inline `style` attributes, which
   normally makes overriding awkward. It does give one clean hook: an inline
   style is a literal, greppable string, so `[style*="…"]` selects exactly the
   elements the designer gave that treatment — no classes to invent, and no
   edits to the design source. Every override below is `!important` because it
   is competing with an inline declaration, which is the one case where that is
   simply how the cascade works.

   Three things live here, none of which the design canvas could express:
     1. responsive behaviour (the canvas is desktop-only)
     2. a mobile navigation drawer
     3. accessibility baselines — focus rings, reduced motion, anchor offsets
   =========================================================================== */

/* --- 0. Self-hosted fonts ------------------------------------------------
   The same latin-subset woff2 files build/pdf.mjs already ships for the
   print PDFs (public/fonts/, see PRINT_FONTS there) — every weight the site
   actually uses: Inter 400/500/600 and JetBrains Mono 400/500. These rules
   replace the Google Fonts stylesheet each page's helmet used to load, so
   no request leaves this origin for a font. font-display: swap — text
   renders in the fallback stack immediately and swaps when the file lands. */
@font-face { font-family: 'Inter'; font-style: normal; font-weight: 400; font-display: swap; src: url(/fonts/inter-400.woff2) format('woff2'); }
@font-face { font-family: 'Inter'; font-style: normal; font-weight: 500; font-display: swap; src: url(/fonts/inter-500.woff2) format('woff2'); }
@font-face { font-family: 'Inter'; font-style: normal; font-weight: 600; font-display: swap; src: url(/fonts/inter-600.woff2) format('woff2'); }
@font-face { font-family: 'JetBrains Mono'; font-style: normal; font-weight: 400; font-display: swap; src: url(/fonts/jetbrains-mono-400.woff2) format('woff2'); }
@font-face { font-family: 'JetBrains Mono'; font-style: normal; font-weight: 500; font-display: swap; src: url(/fonts/jetbrains-mono-500.woff2) format('woff2'); }

/* --- 1. Baselines the design assumes but never states -------------------- */

/* The design paints a #0A0A0B page but never declares a colour scheme, so
   `getComputedStyle(document.documentElement).colorScheme` is `normal` and the
   browser draws its own chrome light: the document scrollbar, and — most
   visibly — the native <select> popups on /request-quote, which open as a white
   panel punched into the black page with near-white <option> text on a
   browser-default light background. `color-scheme` is the only hook that
   reaches browser-drawn UI; it changes nothing the design paints itself.
   No counterpart belongs in doc.css: the four download pages are deliberately
   light-themed, and they never load this file anyway. */
:root { color-scheme: dark; }

/* box-sizing is deliberately NOT reset globally.
   The design canvas is content-box and the designer writes box-sizing:border-box
   inline on the handful of elements that want it (6 of 246 on the home page). A
   `*` reset silently re-measured every padded, fixed-width element on the site —
   a different box model than the one the layout was drawn in. Only the elements
   this build adds get border-box. */
.nav-toggle, .nav-toggle-bar, .skip-link { box-sizing: border-box; }

/* …with one deliberate, narrow exception: the CTA anchors. The design gives
   both halves of every button pair the same `height:`, but only the secondary
   carries `border:1px`, which content-box then adds outside that height — so
   every pair sits 2px out of line (48/50 on "Explore Models"/"Request Quote",
   44/46 on "View Advanced MPG"/"View Basic MPG", 50/52 on "Request Quote"/"See
   Advanced MPG spec"; seven home-page buttons measure 38/44/46/48/50/52/54px).
   Safe to switch here where a `*` reset was not, because these anchors are
   shrink-to-fit: with no stated width, border-box only changes how the stated
   height is apportioned, and re-measures no layout the design was drawn in. It
   also stops the ≤680px full-bleed CTAs (which do get `width:100%`) from
   overhanging their container by their own 2px of border.
   `[style*='height:']` does also match `line-height:`/`min-height:`, but on an
   auto-width anchor with no stated height box-sizing is a no-op, so a stray
   match cannot move anything. Verified against dist/: every `<a>` this selects
   is a button, plus the floating WhatsApp pill, whose 26px radius was drawn for
   the 52px it now actually measures rather than the 54px it did. */
a[style*='display:inline-flex'][style*='height:'] { box-sizing: border-box; }

/* .doc-toolbar / .doc-hint box-sizing lives in doc.css, not here: the 4 doc
   pages (downloads/*) only ever link doc.css, never this file, so a rule for
   them here never reached its target. */

html {
  scroll-behavior: smooth;
  -webkit-text-size-adjust: 100%;
}

/* The header is sticky at 68px, so in-page anchors must clear it or the
   target's heading lands underneath the bar. */
[id] { scroll-margin-top: 88px; }

img { max-width: 100%; }

/* The 4-up thumbnail filmstrip under the product heroes (Basic-MPG /
   Advanced-MPG) squashes each photo into its square cell without this.

   Those thumbnails are the only images the design sizes by clamping BOTH axes
   — `max-height:100%;max-width:100%` inside an `aspect-ratio:1` flex centre —
   and build.mjs's stampImageDimensions stamps `width`/`height` on every
   `/images/*.png` for CLS. Those attributes are presentational hints, so both
   axes become definite lengths, both get clamped to the 115px cell
   independently, and nothing is left to re-derive from the ratio: a 232x280
   pendant renders 115x115, 21% too wide. (The attributes, not the
   `display:contents` <picture> wrapper around them — removing the wrapper
   changes nothing, removing the attributes fixes it.)

   `auto` on both axes hands the sizing back to max-width/max-height, which do
   preserve the ratio, and is what stampImageDimensions already assumes the CSS
   does ("deliberately leaves one axis as auto"). The attributes stay on the
   element, so the browser still reserves a ratio-correct box before decode and
   the CLS win is untouched. Not `object-fit:contain`: that paints correctly but
   leaves the element box square, so the 1px cell border and the photo still
   disagree about where the image ends.

   The two datasheets (design-src/*-Datasheet.dc.html) clamp both axes the same
   way and are distorted by the same cause, but they load doc.css, never this
   file — a rule for them here would never reach them. */
img[style*='max-height:100%'][style*='max-width:100%'] { width: auto; height: auto; }

/* The design has no focus styling at all. Keyboard users get a visible ring
   that matches the brand red, without changing the mouse rendering. */
:focus-visible {
  outline: 2px solid #E30613;
  outline-offset: 3px;
  border-radius: 2px;
}

/* Request-Quote carries its own page-level, design-authored rule —
   `input:focus, select:focus, textarea:focus { outline:none; border-color:#E30613 }`
   — verbatim from design-src. Its (0,1,1) specificity beats the generic
   `:focus-visible` rule above (0,1,0) and deletes the ring for every field on
   that one page. Restated here with the element type folded into the
   selector (raising specificity to match) and `!important` (this file's
   established pattern for winning over inline/page-level declarations)
   so the ring survives regardless of source order. */
input:focus-visible, select:focus-visible, textarea:focus-visible {
  outline: 2px solid #E30613 !important;
  outline-offset: 3px !important;
}

/* The three "envelope" radios on Request-Quote draw their visible border on
   the wrapping <label>, not the <input> — the page rule's border-color swap
   above lands on the input, which has no border to swap, so keyboard focus
   was otherwise invisible on those three controls specifically. Give the
   label itself a ring when the input it wraps has focus-visible. Scoped to
   just those radios (name="envelope") — a generic `label:has(input:focus-visible)`
   also matched every text/select field's wrapping <label>, doubling up with
   the input's own ring above into a distracting nested red box. */
label:has(input[name='envelope']:focus-visible) {
  outline: 2px solid #E30613 !important;
  outline-offset: 2px !important;
}

/* Skip link for keyboard and screen-reader users. */
.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 200;
  padding: 12px 18px;
  background: #E30613;
  color: #fff;
  font: 600 14px/1 'Inter', system-ui, sans-serif;
  border-radius: 0 0 3px 0;
}
.skip-link:focus {
  left: 0;
}

/* No .table-scroll rule: none of the real <table> elements that load this
   stylesheet (the two comparison tables, the per-controller pin-assignment
   tables) need one — each stays within its column, or collapses to a single
   readable column, under the grid rules below rather than scrolling
   sideways. The <table> elements inside <doc-page> on the download routes
   load doc.css instead. The matching JS helper was removed for the same
   reason this comment used to give: it had nothing to attach to.

   .sr-only visually hides an element while leaving it in the accessibility
   tree — used for <caption> text on tables that already sit under a visible
   heading saying the same thing, so the caption is not shown twice. Same
   clip technique build.mjs uses for the quote form's honeypot field. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* Links in running copy were told apart from the text around them by colour
   alone — #FF4A52 on #A0A0A8 body text is 1.72:1, where WCAG 1.4.1 wants 3:1
   when colour is the only non-text cue.

   The red-attribute hook is not redundant with `p a`: two of the twelve
   inline links are not in a <p> at all ("OEM terms →" sits in a <span> inside
   the OEM checkbox label on /request-quote, the /privacy contact line in a
   <div>), so `p a, li a` alone would miss both. `p a, li a` earns its place
   in the other direction, catching the five unstyled "wiring diagrams" links
   in controller prose, which were separated from the sentence around them by
   lightness alone at 3.1:1.

   Audited against dist/: every <a> inside a <p> here is running text — no nav
   item, footer link, breadcrumb, card link or CTA is inside a <p> or an <li>,
   so this cannot reach the chrome. The `display:inline-flex` exclusion is the
   same string the box-sizing rule above uses to spot a button, so a CTA
   dropped into a paragraph later still stays clean. The `text-decoration`
   exclusion honours the designer's inline `none` on the three "Send your
   control model →" card links — no `!important`, so that opt-out keeps
   working. Nothing sets `text-decoration` on :hover or :focus, so the
   underline survives both states. */
p a:not([style*='display:inline-flex']):not([style*='text-decoration']),
li a:not([style*='display:inline-flex']):not([style*='text-decoration']),
a[style*='color:#FF4A52']:not([style*='text-decoration']) {
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* Line length. /privacy sets its prose in a 820px column at 15.5px, which
   measures 103–116 characters a line — comfortable reading is ~45–85, and
   only the lede paragraph had a max-width. The "Source: …" caption under each
   controller pin table is worse: 11.5px across the same 820px is 138–142
   characters.

   58ch, not the 68ch the issue asked for. `ch` is the width of the digit
   zero, and Inter's zero (9.78px at 15.5px type) is far wider than its
   average character here (7.13px measured), so 68ch lands at 665px — still 93
   characters, outside the range this is trying to reach. 58ch is 567px and
   measures 79; the same 58ch at 11.5px is 420px and measures 73 on the
   captions. One value, both in range.

   /privacy gets a class because it has no distinctive inline hook — its
   paragraphs share `font-size:15.5px;line-height:1.7` with /about. On the
   container, so the <li>s and provider-card paragraphs inside are covered
   too; the `·`-separated field lists are <div>s and stay full width, which is
   right, they are scanned rather than read. The captions do have a hook:
   `font-size:11.5px` on a <p> matches exactly the four Source lines
   (/wireless-mpg-for-syntec has none). Both are plain blocks in normal flow,
   so a max-width only shortens the line — no grid track or centred layout is
   measured from either. */
.prose p,
.prose li,
p[style*='font-size:11.5px'] {
  max-width: 58ch;
}

/* --- 2. Mobile navigation ------------------------------------------------ */

.nav-toggle {
  display: none;
  /* The button is emitted immediately before the <nav> it controls, so keyboard
     Tab order runs toggle -> drawer links. `order` puts it back at the right-hand
     edge of the flex bar visually without disturbing that. */
  order: 99;
  width: 42px;
  height: 38px;
  /* Deliberately no `margin-left:auto` here. It was inert above 900px — the
     button is `display:none` at those widths, so it is not a flex item and has
     no free space to claim — and actively wrong at or below 900px, where it
     swallowed all the free space the bar's inline `justify-content:space-between`
     needed and left the logo and the quote button glued together (#28). `order`
     above is enough to keep the button last in the bar; the auto margin that
     pushes it and the quote button to the right edge as a pair now lives on the
     quote button, in the 900px block.
     `flex-shrink:0` because 42x38 is the tap target: when the bar runs short on
     a narrow phone something has to give, and it must not be this (see the
     380px block, which makes the wordmark the one elastic item). */
  flex-shrink: 0;
  padding: 0;
  background: #101012;
  border: 1px solid #232326;
  border-radius: 3px;
  cursor: pointer;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
}
.nav-toggle-bar {
  display: block;
  width: 18px;
  height: 1.5px;
  background: #F2F2F3;
  transition: transform 0.22s ease, opacity 0.18s ease;
}
.nav-toggle[aria-expanded='true'] .nav-toggle-bar:nth-child(1) {
  transform: translateY(5.5px) rotate(45deg);
}
.nav-toggle[aria-expanded='true'] .nav-toggle-bar:nth-child(2) { opacity: 0; }
.nav-toggle[aria-expanded='true'] .nav-toggle-bar:nth-child(3) {
  transform: translateY(-5.5px) rotate(-45deg);
}

/* --- 3. Responsive ------------------------------------------------------- */

/* Wide tablet / small laptop: pull the 1280px container in before it starts
   touching the viewport edges. */
@media (max-width: 1180px) {
  [style*='max-width:1280px'] { padding-left: 24px !important; padding-right: 24px !important; }
  [style*='font-size:76px'] { font-size: 60px !important; }
}

/* Tablet: the two-column page structures stop working. */
@media (max-width: 1000px) {
  /* Hero, and the sticky-aside sections (Problem/solution, FAQ). The mirrored
     `1fr 1.02fr` / `1fr 340px` variants that used to be listed here matched
     nothing in any built page and were removed.

     The last three ratios are the same two-column page structure under
     different weights, and the list had simply never been extended to them:
     1.25fr is the "Supported controllers" split on Basic/Advanced, 1.1fr the
     Wireless-vs-wired hero, 1.05fr the Request-quote hero. Left uncollapsed,
     1.25fr/1fr at 768px squeezed the controller grid nested in its left column
     down to 180.7px cells, wrapping a caption to three lines at 10.5px.
     No trailing `;` is needed to anchor these: no value in dist/ has any of
     them as a prefix — `1.1fr 1fr` in particular cannot match `1.15fr 1fr`,
     because the literal requires `1.1` followed immediately by `fr`. */
  [style*='grid-template-columns:1.02fr 1fr'],
  [style*='grid-template-columns:340px 1fr'],
  [style*='grid-template-columns:320px 1fr'],
  [style*='grid-template-columns:1.15fr 1fr'],
  [style*='grid-template-columns:1.25fr 1fr'],
  [style*='grid-template-columns:1.1fr 1fr'],
  [style*='grid-template-columns:1.05fr 1fr'] {
    grid-template-columns: 1fr !important;
    gap: 44px !important;
  }

  /* The 1.15fr/1fr hero (Basic/Advanced MPG) fakes its column gap with child
     padding instead of a real `gap`, since its `gap:0`. Once the grid above
     collapses to one column, that 56px inner padding is no longer a gutter —
     it is just an extra, asymmetric inset stacked on top of the page's own
     18-24px edge padding. Zero the horizontal component and keep the 48px
     top/bottom for vertical rhythm. */
  [style*='padding:48px 56px 48px 0'],
  [style*='padding:48px 0 48px 56px'] {
    padding-left: 0 !important;
    padding-right: 0 !important;
  }

  /* Same column, second problem. Its product stage is `aspect-ratio:4/3.4`, so
     once the grid above collapses the stage takes the whole page width and its
     height follows: on /basic-mpg the stage is 585x498 at 1280px but 936x796
     at 1000px, and the pendant inside it — sized `height:78%` — is drawn 513px
     wide from a 232px source. A 2.21x upscale, and 1.66x at 768px;
     /advanced-mpg is 1.88x and 1.41x. (The issue's 1.40x/1.70x and 1.19x
     figures miss the peak, which is at 1000px, the last pixel before the grid
     folds.) There is no higher-resolution source to re-export — 232px on the
     short edge is all that has ever existed for these two renders, in
     design-src/, in public/, and back through the original import — so the fix
     is to stop asking for more than there is.

     Capping the image alone left a 232px pendant adrift in a 936px panel, so
     the panel is capped too, at the 600px the stage is drawn at on the desktop
     canvas: it keeps the composition as laid out and takes stage and filmstrip
     down together, so their edges stay aligned. Centred, because a 600px block
     hard against the left of a 952px page reads as a mistake. The vertical
     rule goes with it — it divides columns, and there is no second column here
     to divide from. Residual upscale inside the capped stage is handled by the
     `max-height` on the two hero <img>s, inline in design-src/Basic-MPG.dc.html
     and design-src/Advanced-MPG.dc.html next to the heights they clamp. */
  [style*='padding:48px 56px 48px 0'] {
    max-width: 600px !important;
    margin-left: auto !important;
    margin-right: auto !important;
    border-right: 0 !important;
  }

  /* A sticky aside inside a now-single-column grid just pins mid-scroll. */
  [style*='position:sticky'][style*='top:110px'] { position: static !important; }

  /* The hero is sized to fill the viewport minus the header; on a short
     landscape tablet that clips, so let it size to content. */
  [style*='min-height:calc(100vh - 68px)'] {
    min-height: 0 !important;
    padding-top: 64px !important;
    padding-bottom: 64px !important;
  }

  /* Numeric strips: 5 and 3 across become 2 and 2. The per-cell borders are
     drawn with border-right, which leaves a dangling edge once the row wraps,
     so the grid switches to a gap-and-outer-border treatment. */
  [style*='grid-template-columns:repeat(5,1fr)'] { grid-template-columns: repeat(2, 1fr) !important; }
  [style*='grid-template-columns:repeat(4,1fr)'] { grid-template-columns: repeat(2, 1fr) !important; }
  [style*='grid-template-columns:1.4fr 1fr 1fr 1fr'] {
    grid-template-columns: repeat(2, 1fr) !important;
    gap: 32px !important;
  }

  /* The 5-stat strip's cell padding is positional, drawn for a 5-wide row:
     cell 1 is flush-left (`38px 24px 34px 0`), cells 2-4 are inset both sides,
     cell 5 is flush-right (`38px 0 34px 24px`). Once the row above folds to two
     columns those insets no longer describe where the cells actually are —
     measured at 768px, cell 1's text starts at x=24 while cells 3 and 5 start
     at x=48. Normalise all five to one symmetric inset so each column reads to
     a single left edge; the ≤680px block re-normalises them again, tighter,
     for the single-column case (both rules are (0,1,0), so the later one wins
     there — do not raise the specificity of either). */
  [style*='padding:38px 24px 34px'],
  [style*='padding:38px 0 34px 24px'] { padding: 34px 22px 30px !important; }

  /* Same strip: the dividers are per-cell `border-right`, which in two columns
     is correct for column 1 but draws a stray hairline down the strip's own
     right edge for every cell in column 2. Cells 2 and 4 are that column;
     cell 5 (last, and alone on its row) never carried a border to begin with.
     This deliberately keeps applying below 680px, where a stacked cell has
     nothing to its right either. */
  [style*='grid-template-columns:repeat(5,1fr)'] > :nth-child(even) {
    border-right: 0 !important;
  }

  /* The 4-up thumbnail strip under the Basic/Advanced hero image is the one
     `repeat(4,1fr)` that must not halve: it is a filmstrip of four `aspect-
     ratio:1` product shots, and two columns turns it into two ~354px squares.
     Anchored on `gap:12px`, which separates it from the five per-controller
     Downloads grids — those are `repeat(4,1fr);gap:1px` text-link cards and do
     want to fold. Declared after the blanket rule above and sharing its
     specificity, so source order decides. */
  [style*='grid-template-columns:repeat(4,1fr);gap:12px'] {
    grid-template-columns: repeat(4, 1fr) !important;
  }

  /* The 3-up hairline card grids (9 of them) were never in this list at all,
     so at 768px they stayed 3 across at 238px per cell, sitting directly under
     `repeat(2,1fr)` rows of ~353px on the same page. Two across matches those
     rows and gives the card text back its measure. */
  [style*='grid-template-columns:repeat(3,1fr)'] { grid-template-columns: repeat(2, 1fr) !important; }

  /* …but those grids fake their hairlines with `gap:1px` over a #1C1C20
     container background, so an unfilled cell is not blank — it paints the
     hairline colour. Six of the nine hold exactly three cards, which in two
     columns leaves the second half of row 2 as a solid grey block. Let a card
     that is both third and last span the row instead. `:nth-child(3):last-
     child` is true only for those six; the three six-card grids tile 2x3 and
     are untouched. Its higher specificity is harmless below 680px: on the
     single-column grid the blanket rule produces, `1 / -1` resolves to
     column 1, which is where the card already was. */
  [style*='grid-template-columns:repeat(3,1fr)'] > :nth-child(3):last-child {
    grid-column: 1 / -1 !important;
  }

  /* Section rhythm tightens. */
  [style*='padding:110px 0'] { padding: 72px 0 !important; }
  [style*='padding:120px 0'] { padding: 76px 0 !important; }
  [style*='padding:84px 0'] { padding: 60px 0 !important; }

  /* Type scale. These key on the literal value the design wrote, so a size
     that is not listed simply renders at its desktop measurement. This block
     covers the display sizes (>=40px) only — 96, 58, 56 and 48 were missing,
     which is why /404's h1 stayed 96px, Wireless-vs-wired's stayed 58px and
     Basic/Advanced's section h2 stayed 48px. The sizes below 40px are left to
     the ≤680px block on purpose: 38px and down still fit the ~720px column a
     tablet gives, and shrinking 22px here while the neighbouring 24px rule
     (also ≤680px only) held would have inverted the two.
     No substring hazard in any of these: `font-size:32px` cannot match
     `font-size:132px`, which would need `font-size:` immediately followed by
     `32px`, and no element in dist/ carries two font-size declarations in one
     style attribute. The clamps are tuned to hand over continuously to the
     ≤680px block at exactly 680px. */
  [style*='font-size:96px'] { font-size: clamp(54px, 8.4vw, 72px) !important; }
  [style*='font-size:76px'] { font-size: clamp(40px, 7.4vw, 56px) !important; }
  [style*='font-size:58px'] { font-size: clamp(40px, 5.6vw, 46px) !important; }
  [style*='font-size:56px'] { font-size: clamp(38px, 5.4vw, 45px) !important; }
  [style*='font-size:52px'] { font-size: clamp(32px, 5.4vw, 42px) !important; }
  [style*='font-size:48px'] { font-size: clamp(30px, 5vw, 39px) !important; }
  [style*='font-size:44px'] { font-size: 36px !important; }
  [style*='font-size:42px'] { font-size: 34px !important; }
  [style*='font-size:40px'] { font-size: clamp(27px, 4vw, 34px) !important; }
}

/* Header collapses to a drawer. Kept at the same breakpoint the nav actually
   runs out of room at. */
@media (max-width: 900px) {
  .site-header > div {
    gap: 12px !important;
    padding-left: 20px !important;
    padding-right: 20px !important;
  }

  /* #16. The toggle only appears once JS has proven it can drive it. The
     inline script at the top of <head> (build.mjs, JS_CLASS_SCRIPT) swaps
     `html.no-js` for `html.js` before first paint; with JS off that swap
     never happens, the button stays `display:none` (its unconditional base
     rule above) and .site-nav (below) stays visible in normal flow instead —
     a dead toggle would only hide the only way to reach it. */
  html.js .nav-toggle { display: flex; }

  .site-nav {
    flex-direction: column !important;
    align-items: stretch !important;
    gap: 0 !important;
    padding: 8px 20px 20px !important;
    background: #0C0C0E;
    border-bottom: 1px solid #232326;
  }

  /* Without JS, .site-nav keeps only the plain rules above: a normal flex
     item, full width, stacked below the logo/quote-button row (flex-basis
     100% forces the wrap) — always visible, nothing to open. */
  html:not(.js) .site-header > div { flex-wrap: wrap !important; height: auto !important; }
  html:not(.js) .site-nav { flex-basis: 100%; width: 100%; }

  /* With JS, the drawer collapses to an absolutely-positioned overlay driven
     by `data-open` (site.js), inert while closed so a closed drawer cannot be
     reached by tabbing. */
  html.js .site-nav {
    position: absolute !important;
    top: 100%;
    left: 0;
    right: 0;
    box-shadow: 0 24px 40px -20px rgba(0, 0, 0, 0.9);
    display: none !important;
  }
  html.js .site-nav[data-open='true'] { display: flex !important; }

  .site-nav > a {
    padding: 15px 2px !important;
    border-bottom: 1px solid #1C1C20 !important;
    font-size: 15.5px !important;
    color: #E8E8ED !important;
  }

  /* The header's own quote button stays visible next to the toggle, but the
     label shortens so both fit a narrow bar.

     Matched on the design's own red fill rather than on the href: on
     /request-quote that same button is a self-link to the form on that page
     (`href="#form"`, emitted by build.mjs), so `a[href='/request-quote']`
     styled 13 of the 14 built headers and silently skipped the one page the
     button matters most on. Exactly one anchor per header carries
     `background:#E30613` — verified across every page in dist/ — and the
     `.site-header` scope keeps the body's own red CTAs out of it. */
  .site-header a[style*='background:#E30613'] {
    padding: 0 14px !important;
    font-size: 12.5px !important;

    /* #28. The bar's inline `justify-content:space-between` can only
       distribute free space that no item has already claimed with an auto
       margin — and the toggle used to claim all of it, so at 768px the logo
       (x 20-161) and this button (x 173-292) ended up 12px apart at the left
       with a ~400px dead gap before the toggle. The auto margin belongs here:
       it pushes this button, and the toggle that follows it visually via
       `order:99`, to the right edge as a pair, and leaves the free space
       where it reads as intentional — between the brand and the actions.
       No `!important`: the design sets no margin on this anchor inline. */
    margin-left: auto;

    /* #31. DOM order is logo -> toggle -> drawer links -> this button, so
       Tab visits the toggle before this button. Without an explicit order
       the button's default (0) put it visually *before* the toggle
       (order:99) — a sighted keyboard user would see focus jump from the
       toggle at the far right back to this button in the middle. `98` keeps
       it just left of the toggle, the two ending up adjacent at the right
       edge, so the visual layout matches the tab stop that comes right
       after it. */
    order: 98;
  }
}

/* Phone. */
@media (max-width: 680px) {
  [style*='max-width:1280px'] { padding-left: 18px !important; padding-right: 18px !important; }

  /* Everything single column, including the 2-up card rows and the numeric
     strips that survived the tablet step. `minmax(0,1fr)` rather than a bare
     `1fr`: a bare `1fr` track still has an automatic minimum of `auto`, which
     is the track's content min-content size. Most stacked content (text,
     wrapping divs) has a small min-content size so this never mattered, but a
     native `<input type=file>` has its own unshrinkable chrome (~305px
     min-content) — on the request-quote form that forced its single-column
     track to 305px even where only ~284px of viewport was available, pushing
     the page into horizontal scroll. `minmax(0,1fr)` floors the automatic
     minimum at 0 so the track fills the available space and the input's own
     `width:100%` (below) shrinks to fit, same as every other 1fr grid on the
     site is unaffected since ordinary content never needed the auto minimum. */
  [style*='grid-template-columns'] { grid-template-columns: minmax(0, 1fr) !important; }

  /* The track fix above isn't sufficient on its own: the file input's direct
     parent is a `<label style="display:grid">`, a second, un-widened grid
     whose single implicit column still defaults to an auto minimum sized to
     the input's own ~305px intrinsic chrome. Give the input an explicit
     width and a zeroed min-width so it shrinks with its column instead of
     forcing it wide. */
  input[type='file'] { width: 100% !important; min-width: 0 !important; }

  /* …except the two comparison tables, which are grids of table ROWS. Stacking
     a row turns "Working reach / 3–5 m fixed / 50 m clear line of sight" into
     three unlabelled lines with no way to tell which column a value came from.
     They stay tabular and shrink instead.

     The trailing `;` in these selectors is load-bearing: without it,
     `1.4fr 1fr 1fr` also matches the footer's `1.4fr 1fr 1fr 1fr`, because an
     attribute-substring selector has no word boundary. Both rules below are
     declared after the blanket rule and share its specificity, so source order
     decides — do not move them above it. */
  [style*='grid-template-columns:1.2fr 1fr 1fr;'],
  [style*='grid-template-columns:1.4fr 1fr 1fr;'] {
    grid-template-columns: 1.2fr 1fr 1fr !important;
    gap: 8px !important;
    font-size: 12.5px !important;
  }
  [style*='grid-template-columns:1.2fr 1fr 1fr;'] > *,
  [style*='grid-template-columns:1.4fr 1fr 1fr;'] > * {
    font-size: 12.5px !important;
    line-height: 1.4 !important;
    padding-left: 0 !important;
    padding-right: 0 !important;
    word-break: break-word;
  }

  /* …and nor does the 4-up thumbnail strip under the Basic/Advanced hero
     image, for the same reason the comparison tables don't: stacking it
     destroys what it is. Four `aspect-ratio:1` product shots became four
     stacked full-width images — measured 323x323px at 375px — adding 1328px of
     scroll to what the design draws as a compact filmstrip. Held at four
     across the same row is ~76px per thumbnail, in proportion to the ~139px
     the strip gets on the 1280px canvas.
     `gap:12px` is what separates it from the five per-controller Downloads
     grids, which are `repeat(4,1fr);gap:1px` text-link cards and do stack.
     Declared after the blanket rule and sharing its specificity, so source
     order decides — same pitfall as the tables above. */
  [style*='grid-template-columns:repeat(4,1fr);gap:12px'] {
    grid-template-columns: repeat(4, 1fr) !important;
  }

  /* The bordered cell grids use 1px gaps over a background to fake hairlines;
     that survives stacking, so only the outer rhythm needs adjusting. */
  [style*='padding:110px 0'] { padding: 56px 0 !important; }
  [style*='padding:120px 0'] { padding: 58px 0 !important; }
  [style*='padding:84px 0'] { padding: 48px 0 !important; }
  [style*='padding:80px 32px'] { padding: 48px 18px !important; }

  /* Ten sizes the phone scale never covered — 96, 58, 56, 48, 38, 36, 34, 32,
     28 and 22 — all verified present in dist/. Each clamp starts where the
     ≤1000px block leaves off at 680px so nothing jumps at the breakpoint; the
     flats below 40px follow the existing 24px/19px rules' judgement that small
     sizes do not need to track the viewport.
     At 375px this puts every page title in the 28-39px band (96→39, 76→35.25,
     58→32.25, 56→30.75, 52→28.5, 48→28), so the h1s stay ordered site-wide.
     The one crossing left standing is the per-controller pages, where the h1
     (48px) lands just under a mid-page h2 (42px, held at a flat 30px by the
     rule below because the same value is five JetBrains Mono stat numerals on
     the home page). They are sections apart and never on screen together. */
  [style*='font-size:96px'] { font-size: clamp(40px, 10.4vw, 54px) !important; }
  [style*='font-size:76px'] { font-size: clamp(33px, 9.4vw, 44px) !important; }
  [style*='font-size:58px'] { font-size: clamp(31px, 8.6vw, 40px) !important; }
  [style*='font-size:56px'] { font-size: clamp(30px, 8.2vw, 38px) !important; }
  [style*='font-size:52px'] { font-size: clamp(27px, 7.6vw, 36px) !important; }
  [style*='font-size:48px'] { font-size: clamp(28px, 7.2vw, 34px) !important; }
  [style*='font-size:44px'],
  [style*='font-size:42px'] { font-size: 30px !important; }
  [style*='font-size:40px'] { font-size: clamp(24px, 6.4vw, 29px) !important; }
  [style*='font-size:38px'] { font-size: 25px !important; }
  [style*='font-size:36px'] { font-size: 24px !important; }
  [style*='font-size:34px'] { font-size: 23px !important; }
  [style*='font-size:32px'] { font-size: 22px !important; }
  [style*='font-size:28px'] { font-size: 21px !important; }
  [style*='font-size:24px'] { font-size: 21px !important; }
  [style*='font-size:22px'] { font-size: 20px !important; }
  [style*='font-size:19px'] { font-size: 17.5px !important; }

  /* Cell padding inside the hairline grids. */
  [style*='padding:32px 30px'] { padding: 24px 22px !important; }
  [style*='padding:34px 30px'] { padding: 26px 22px !important; }
  [style*='padding:26px 24px 24px'] { padding: 22px 20px 20px !important; }
  /* The home page's 5-stat strip: cells 1-4 (`38px 24px 34px 0` on the
     first cell, `38px 24px 34px` on the middle two) share this substring and
     get reset together, which incidentally also gives cell 1 the same 20px
     left inset as its siblings once the strip stacks to one column. The 5th
     cell's edge-flush value, `38px 0 34px 24px`, doesn't share that
     substring, so it never matched and stayed flush against the right edge
     while cells 1-4 gained a left/right inset. List it explicitly so all
     five cells land on the same padding. */
  [style*='padding:38px 24px 34px'],
  [style*='padding:38px 0 34px 24px'] { padding: 26px 20px 24px !important; }
  /* Same strip, the other half of the same problem: the per-cell `border-right`
     that drew the 5-up dividers has nothing to divide once the cells are
     stacked full-width, and lines up into one hairline down the strip's right
     edge for cells 1-4 (cell 5 never had one). The ≤1000px block already zeroes
     it on the even cells for the two-column case; zero the rest here. */
  [style*='grid-template-columns:repeat(5,1fr)'] > * { border-right: 0 !important; }
  /* `$=` (ends-with), not `*=`: the two home-page cards this targets have
     `padding:30px` as their entire (and thus final) style declaration, but
     `padding:30px` is also a strict prefix of the six wireless-vs-wired-mpg
     cards' `padding:30px 28px` — a bare `*=` substring match collapsed their
     intentional asymmetric padding too. Neither value has a trailing `;` to
     anchor on (both are their style attribute's last declaration), so anchor
     on the end of the attribute instead. */
  [style$='padding:30px'] { padding: 24px !important; }

  /* Full-width CTAs read better than shrink-wrapped ones on a phone. */
  [style*='height:48px'][style*='padding:0 26px'],
  [style*='height:50px'][style*='padding:0 30px'] {
    width: 100% !important;
    justify-content: center !important;
  }
}

/* Narrow phone: the header bar runs out of room (#29).

   Measured at the 900px layout, the bar is three near-rigid items — logo 141px
   (22px dot + 10px gap + 109px wordmark), quote button 119px, toggle 42px —
   plus two 12px gaps and the 20px insets: 366px of bar. At 360px, the most
   common Android width, it is 6px short, and the only parts that can give are
   the two text labels, so both wrap: the wordmark to "Wireless / MPG" (25px
   tall becomes 39px) and the button's label to two lines inside a fixed 38px
   box, 4px of clearance left. Nothing overflows — it just stops reading as a
   header. Bisected: single line at a 370px content box, wrapped at 360px.

   The query fires at 380px rather than 366px because a media query's `width`
   includes the classic scrollbar gutter that the content box does not (the
   bisection above emulated a phone's overlay scrollbar with
   `scrollbar-width:none`), so it has to be early enough to still catch a
   366px content box in a desktop window. Between 366px and 380px nothing was
   wrapping, so all the block does there is tighten the bar by a millimetre —
   a fair price for firing early enough to be reliable.

   Shortening the button's label to "Quote" would have been the other fix, but
   that is markup, and this file exists so the design source does not have to
   change. */
@media (max-width: 380px) {
  /* The block reclaims ~23px in total, which moves the point where the bar
     stops fitting from 366px down to ~343px. 8px of it comes out of spacing
     before any type is touched: 2px off each gap, and an inset of 18px — the
     same one the page's own containers already use below 680px. */
  .site-header > div {
    gap: 10px !important;
    padding-left: 18px !important;
    padding-right: 18px !important;
  }

  /* The wordmark. Two hrefs because the design links the logo to Home.dc.html
     and build.mjs rewrites that to the published route — which is the in-page
     `#top` anchor on the page that IS Home, and `/` on the other 13 headers.
     Checked against dist/: exactly one such anchor per header, and the
     wordmark is its last child span (the first is the dot). A rule that
     matched only one of the two spellings would look fixed and not be.

     15px is ~7px narrower and still reads as the 16px original next to 12.5px
     nav type. `overflow`/`text-overflow` are the safety net rather than the
     intent: see the shrink note below. */
  .site-header a[href='/'] > span:last-child,
  .site-header a[href='#top'] > span:last-child {
    white-space: nowrap;
    font-size: 15px !important;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  /* The quote button, same treatment, plus 4px off each side of the padding —
     the remaining ~8px. Selector rationale is in the 900px block. */
  .site-header a[style*='background:#E30613'] {
    white-space: nowrap;
    padding: 0 10px !important;
  }

  /* Which item gives way when even a 343px bar does not fit — an iPhone SE 1
     at 320px, a folded phone at 280px. `nowrap` converts the old wrap into an
     overflow, and the page wrapper is `overflow-x:hidden`, so an overflowing
     bar would clip the menu button off the right edge with no way to scroll to
     it: a worse failure than the wrap this block is fixing. So pin everything
     that must stay whole and leave exactly one elastic item, the wordmark,
     which truncates with an ellipsis:
       - the toggle is pinned with `flex-shrink:0` in section 2;
       - the button's label is `nowrap`, so its own automatic minimum size is
         already its full width and it cannot shrink;
       - the dot is a fixed 22px circle, but an empty box has a min-content
         size of 0, so flex would happily squash it to an ellipse.
     `min-width:0` on the anchor as well as on the wordmark: without it the
     anchor's automatic minimum is its own min-content width, which with
     `nowrap` is the untruncated wordmark, and neither would ever shrink. The
     bar now only starts overflowing below ~231px. */
  .site-header a[href='/'],
  .site-header a[href='#top'] { min-width: 0; }
  .site-header a[href='/'] > span:first-child,
  .site-header a[href='#top'] > span:first-child { flex-shrink: 0; }
}

/* --- 4. Motion ----------------------------------------------------------- */

/* The design ships two animations of its own — the hero's rising entrance and
   the 4.5s pulsing ring — plus a count-up in Home's dc-script. Section 6 adds
   the rest of the motion system on top. Everything in both is either
   time-based or scroll-driven, so both are switched off here.
   `animation-duration` does NOT reach a scroll-driven animation: its progress
   comes from a timeline, not from a clock, so a 0.001ms duration leaves it
   sitting at whatever the scroll position says — which for a reveal means
   permanently invisible. Section 6 therefore declares every scroll-driven rule
   inside `(prefers-reduced-motion: no-preference)` rather than relying on this
   block to undo it. */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
}

/* --- 5. Print ------------------------------------------------------------ */

/* Printing a marketing page is not a designed case — the design paints its
   colours and panel backgrounds inline, so a `body { background:#fff }` override
   reached only inherited text and produced either near-black text on near-black
   panels or pale grey on white. Rather than half-invert it, force the whole
   document to plain black-on-white and drop the decorative fills. Anyone wanting
   a designed printable has the four PDFs. */
@media print {
  .site-header, .nav-toggle, .skip-link { display: none !important; }

  html, body { background: #fff !important; }

  body *, body *::before, body *::after {
    background-image: none !important;
    background-color: transparent !important;
    color: #111 !important;
    box-shadow: none !important;
    text-shadow: none !important;
    filter: none !important;
  }

  /* Keep structure legible once the fills are gone. */
  body *[style*='border'] { border-color: #bbb !important; }
  a { text-decoration: underline; }
  img { max-width: 60% !important; height: auto !important; }
}

/* --- 6. Motion system ----------------------------------------------------
   The site reads as a printed brochure: correct, dense, and completely still.
   This section gives it a pulse without touching a line of design source.

   Two rules govern everything below.

   1. **No new markup, no new JavaScript.** Every hook is an inline-style
      substring the design already wrote — the same technique section 3 uses to
      make a desktop-only canvas responsive. `[style*='gap:1px;background:#1C1C20']`
      is every hairline card grid on the site; `a[style*='display:inline-flex'][style*='height:']`
      is every button. Nothing here can drift out of sync with markup it does
      not own, and nothing here can fail to load.

   2. **Scroll-driven, not observer-driven.** The reveals use
      `animation-timeline: view()` / `scroll(root)`, which the compositor runs
      off the scroll position directly. That matters for more than smoothness:
      an IntersectionObserver reveal has to hide its targets before it can
      animate them, so a stylesheet that lands while the script does not leaves
      a page of invisible text. A scroll-driven animation is declared inside
      `@supports`, so a browser without the feature simply never hides
      anything — it renders exactly the static page it renders today, and the
      hover, press and transmitter motion below still works there.

   Timing. Entrances are 18-26% of a cover range (~150-220px of scroll):
   present, over before it is noticed. Pointer feedback is 120-180ms with a
   60ms press — the "snappy" end, where the interface answers faster than the
   eye tracks it. Nothing loops except the transmitter ping and the detent
   ring, both of which are the brand saying what the product does. */

:root {
  /* Decelerating, slightly overshoot-free — the motion of a machine axis
     coming to a commanded stop rather than a bouncing UI. */
  --wmpg-snap: cubic-bezier(0.16, 0.84, 0.36, 1);
  --wmpg-out: cubic-bezier(0.33, 1, 0.68, 1);
}

@keyframes wmpg-reveal {
  from { opacity: 0; transform: translate3d(0, 22px, 0); }
  to   { opacity: 1; transform: none; }
}

/* A transmitter ping, not a decorative halo: it starts exactly on the mark's
   own 1.5px ring and leaves, which is the shape of a packet going out. */
@keyframes wmpg-ping {
  0%        { transform: scale(1);   opacity: 0.55; }
  70%, 100% { transform: scale(2.7); opacity: 0; }
}

@keyframes wmpg-spin { to { transform: rotate(360deg); } }
@keyframes wmpg-spin-back { to { transform: rotate(-360deg); } }

@keyframes wmpg-scale-x { from { transform: scaleX(0); } to { transform: scaleX(1); } }

@keyframes wmpg-drawer {
  from { opacity: 0; transform: translate3d(0, -10px, 0); }
  to   { opacity: 1; transform: none; }
}

/* --- 6.1 Pointer feedback -------------------------------------------------
   Available in every browser, and the half of this section that most changes
   how the site feels to use. Guarded on `(hover:hover)` so a touch device
   never latches a hover state after a tap. */

/* Buttons. Same hook the box-sizing rule in section 1 uses to identify one:
   an inline-flex anchor with a stated height. The lift is 1px — enough to
   read as a response, small enough that a row of buttons does not appear to
   wobble. The press undercuts it in 60ms, which is what makes the control
   feel mechanical rather than animated. */
a[style*='display:inline-flex'][style*='height:'],
button[type='submit'],
.nav-toggle {
  transition:
    transform 140ms var(--wmpg-snap),
    box-shadow 200ms var(--wmpg-out),
    background-color 160ms linear,
    border-color 160ms linear,
    color 160ms linear;
}

@media (hover: hover) {
  a[style*='display:inline-flex'][style*='height:']:hover,
  button[type='submit']:hover {
    transform: translateY(-1px);
  }

  /* The red CTAs get the glow; everything else gets a lighter shell. Keyed on
     the design's own brand fill, the same string section 3 uses to find the
     header's quote button. */
  a[style*='background:#E30613']:hover,
  button[type='submit']:hover {
    box-shadow: 0 8px 24px -10px rgba(227, 6, 19, 0.85);
  }

  a[style*='display:inline-flex'][style*='height:']:not([style*='background:#E30613']):hover {
    border-color: #4A4A52 !important;
    background-color: #17171A !important;
  }

  .nav-toggle:hover { border-color: #3A3A42; background: #17171A; }
}

a[style*='display:inline-flex'][style*='height:']:active,
button[type='submit']:active,
.nav-toggle:active {
  transform: translateY(0) scale(0.985);
  transition-duration: 60ms;
}

/* Hairline card cells — the `gap:1px` over a #1C1C20 background that fakes the
   grid rules. Hovering one lights the cell and drops a 2px red rule along its
   top edge, drawn with an inset shadow rather than a pseudo-element so the
   cells never need `position:relative` (several hold absolutely-positioned
   badges that are anchored further up). */
[style*='gap:1px;background:#1C1C20'] > * {
  transition:
    background-color 180ms linear,
    box-shadow 220ms var(--wmpg-out);
}

@media (hover: hover) {
  [style*='gap:1px;background:#1C1C20'] > *:hover {
    background-color: #111114 !important;
    box-shadow: inset 0 2px 0 #E30613;
  }
}

/* Product / model cards. The image scale is inside the card's own
   `overflow:hidden`, so it crops rather than spilling.

   The lift is `translate`, not `transform`, and that is the whole reason this
   rule reads oddly. These cards are also reveal targets (section 6.2), and a
   CSS animation outranks every author declaration for the property it
   animates — including an `!important` one — for as long as it applies, which
   with `animation-fill-mode: both` is forever. So `transform:translateY(-3px)`
   on :hover was silently dead: measured, the card did not move a pixel.
   `translate` is a separate, independently animatable property that composes
   with `transform`, so the reveal keeps the one and the hover gets the other.
   Any hover effect added to a revealed element later has the same problem and
   the same fix. */
[style*='background:#0E0E10'][style*='border-radius:6px'] {
  transition:
    translate 280ms var(--wmpg-snap),
    border-color 220ms linear,
    box-shadow 280ms var(--wmpg-out);
}
[style*='background:#0E0E10'][style*='border-radius:6px'] img {
  transition: transform 420ms var(--wmpg-snap);
}

@media (hover: hover) {
  [style*='background:#0E0E10'][style*='border-radius:6px']:hover {
    translate: 0 -3px;
    border-color: #3A3A42 !important;
    box-shadow: 0 26px 44px -30px rgba(0, 0, 0, 0.95);
  }
  [style*='background:#0E0E10'][style*='border-radius:6px']:hover img {
    transform: scale(1.035);
  }
}

/* Header nav. The design already reserves the space — every nav link carries
   `border-bottom:1.5px solid transparent` — so the underline costs no layout,
   it only has to be given a colour and something to cross-fade with. */
.site-nav > a {
  transition: color 160ms linear, border-bottom-color 200ms var(--wmpg-out);
}
@media (hover: hover) {
  .site-nav > a:hover { border-bottom-color: #E30613 !important; }
}

/* Running-copy links: the underline (section 1) warms towards the brand red
   rather than snapping to it. */
p a, li a { transition: color 150ms linear, text-decoration-color 150ms linear; }

/* Form controls. The design's own `:focus` rule swaps the border to #E30613;
   this only gives it a ramp, and adds the same ramp on hover. */
input, select, textarea {
  transition: border-color 160ms linear, background-color 160ms linear;
}

/* FAQ. The glyph is the design's own "+", flipped to "−" by site.js; the
   summary row gets a hover state it never had. */
details > summary { cursor: pointer; transition: color 160ms linear; }
details > summary span[aria-hidden='true'] {
  display: inline-block;
  transition: transform 220ms var(--wmpg-snap);
}
@media (hover: hover) {
  details > summary:hover { color: #FFFFFF; }
  details > summary:hover span[aria-hidden='true'] { transform: scale(1.3); }
}

/* --- 6.2 Reveal, header and brand motion ---------------------------------
   Everything below is either scroll-driven or an infinite loop, so the whole
   block is opt-out for `prefers-reduced-motion` — see the note in section 4
   for why the global animation-duration clamp cannot do that job here. */
@media (prefers-reduced-motion: no-preference) {

  /* The transmitter ping. The brand mark is a red ring with a filled centre —
     a radio, drawn — and 27 of them sit across the site in the header and
     footer of every page. Two rings leaving it on a 3.4s cycle, offset by
     1.1s, is the only motion on the site that never stops, and the only one
     that states what the product is. `inset:-1.5px` starts each ring exactly
     on the border of the mark itself; `pointer-events:none` keeps it out of
     the logo's hit area. */
  [style*='border:1.5px solid #E30613'] {
    /* The design already writes `position:relative` on this span inline; the
       declaration is repeated so a mark added later without it still anchors
       its own rings rather than the page. */
    position: relative;
  }
  [style*='border:1.5px solid #E30613']::before,
  [style*='border:1.5px solid #E30613']::after {
    content: '';
    position: absolute;
    inset: -1.5px;
    border: 1.5px solid #E30613;
    border-radius: 50%;
    pointer-events: none;
    animation: wmpg-ping 3.4s cubic-bezier(0.2, 0.6, 0.3, 1) infinite;
  }
  [style*='border:1.5px solid #E30613']::after { animation-delay: 1.1s; }

  /* The hero's concentric rings are three static circles behind the product
     photo. Two of them become the machine half of the same idea: dashed, so
     they read as handwheel detents, and turning — the outer one on a slow
     clock, the inner one driven by the scroll position, counter-rotating.
     Two rates rather than one is what stops it looking like a spinner. The
     third ring, the red one, is the design's own 4.5s emission pulse and is
     left exactly as drawn. Both hooks are unique in the built site: one
     element each, both in the home hero. */
  [style*='border:1px solid #151518'][style*='border-radius:50%'] {
    border-style: dashed !important;
    border-color: #202024 !important;
    animation: wmpg-spin 90s linear infinite;
  }
  [style*='border:1px solid #1B1B1F'][style*='border-radius:50%'] {
    border-style: dashed !important;
    border-color: #2A2A30 !important;
  }

  @supports (animation-timeline: scroll(root block)) {
    /* Scroll progress. A 2px rule along the bottom edge of the sticky header,
       scaled straight off the document's own scroll timeline — no listener,
       no rAF, no layout read. On a 14-page site where most pages are long
       technical reads, it is the one piece of chrome that says how much is
       left. */
    .site-header::after {
      content: '';
      position: absolute;
      left: 0;
      right: 0;
      bottom: -1px;
      height: 2px;
      background: linear-gradient(90deg, #E30613, #FF4A52);
      transform-origin: 0 50%;
      transform: scaleX(0);
      animation: wmpg-scale-x linear both;
      animation-timeline: scroll(root block);
    }

    /* …and the bar itself firms up over the first 90px, so a page that has
       been scrolled looks different from one that has not. A CSS animation
       outranks an inline declaration in the cascade, which is why this needs
       no `!important` against the design's inline `background:rgba(10,10,11,0.82)`.
       Only a `to` keyframe: the implicit `from` is whatever the design wrote,
       so the two never have to be kept in step. */
    .site-header {
      animation: wmpg-header linear both;
      animation-timeline: scroll(root block);
      animation-range: 0 90px;
    }
    @keyframes wmpg-header {
      to {
        background-color: rgba(9, 9, 10, 0.95);
        border-bottom-color: #2E2E33;
        box-shadow: 0 14px 34px -26px rgba(0, 0, 0, 0.95);
      }
    }

    /* The inner detent ring is jogged by the scroll wheel rather than a clock:
       a full turn over the length of the page, backwards against the outer
       ring. Scrolling the home page turns a handwheel, which is the product. */
    [style*='border:1px solid #1B1B1F'][style*='border-radius:50%'] {
      animation: wmpg-spin-back linear both;
      animation-timeline: scroll(root block);
    }
  }

  @supports (animation-timeline: view()) {
    /* Section entrances.

       One shared animation, applied through the design's own inline-style
       hooks, so the list below is also a readable inventory of what a section
       is made of on this site:
         letter-spacing:0.14em   the mono eyebrow above every section heading
         letter-spacing:-0.03em  the display h2 under it
         gap:1px;background      each cell of a hairline card grid
         background:#0E0E10      a product / model card
         repeat(5,1fr)           each cell of the home page's stat strip
         details                 each FAQ row

       `cover` rather than `entry`: its range is the element's height plus the
       viewport's, so a tall block and a short one travel a comparable distance
       before they finish, and an element already on screen at load is past the
       end of the range and renders at rest. Nothing above the fold ever
       animates in, which is the point — the hero has its own entrance and the
       page must not appear to assemble itself under the reader. */
    [style*='letter-spacing:0.14em'],
    [style*='letter-spacing:-0.03em'],
    [style*='gap:1px;background:#1C1C20'] > *,
    [style*='background:#0E0E10'][style*='border-radius:6px'],
    [style*='grid-template-columns:repeat(5,1fr)'] > *,
    details {
      animation: wmpg-reveal linear both;
      animation-timeline: view();
      animation-range: cover 2% cover 20%;
    }

    /* Stagger. Sibling cells share a scroll position, so a shifted range is
       what separates them — 2.5% of a cover range is roughly 25px of scroll
       each, which reads as a sweep across the row rather than six things
       arriving at once. Capped at the sixth cell: past that the last card in
       a long grid would still be waiting when the reader has moved on. */
    [style*='gap:1px;background:#1C1C20'] > :nth-child(2),
    [style*='grid-template-columns:repeat(5,1fr)'] > :nth-child(2),
    details:nth-child(2) { animation-range: cover 4.5% cover 22.5%; }

    [style*='gap:1px;background:#1C1C20'] > :nth-child(3),
    [style*='grid-template-columns:repeat(5,1fr)'] > :nth-child(3),
    details:nth-child(3) { animation-range: cover 7% cover 25%; }

    [style*='gap:1px;background:#1C1C20'] > :nth-child(4),
    [style*='grid-template-columns:repeat(5,1fr)'] > :nth-child(4),
    details:nth-child(4) { animation-range: cover 9.5% cover 27.5%; }

    [style*='gap:1px;background:#1C1C20'] > :nth-child(5),
    [style*='grid-template-columns:repeat(5,1fr)'] > :nth-child(5),
    details:nth-child(5) { animation-range: cover 12% cover 30%; }

    [style*='gap:1px;background:#1C1C20'] > :nth-child(n + 6),
    [style*='grid-template-columns:repeat(5,1fr)'] > :nth-child(n + 6),
    details:nth-child(n + 6) { animation-range: cover 14.5% cover 32.5%; }

    /* Hero parallax. The product photograph and its shadow drift up a little
       slower than the page, which separates the subject from the grid behind
       it without moving anything the reader is trying to read. 4% of the hero
       image's own height at most. */
    #top img { animation: wmpg-hero-drift linear both; animation-timeline: view(); }
    @keyframes wmpg-hero-drift {
      from { transform: translate3d(0, -14px, 0); }
      to   { transform: translate3d(0, 30px, 0); }
    }
  }

  /* The mobile drawer opens rather than appears. */
  html.js .site-nav[data-open='true'] {
    animation: wmpg-drawer 220ms var(--wmpg-snap) both;
  }

  /* Native <details> animates its own open/close where the browser can
     interpolate to `auto`. Scoped to :root because `interpolate-size` is
     inherited and only ever acts on a property that is already transitioning —
     nothing else on the site transitions a size. */
  @supports (interpolate-size: allow-keywords) {
    :root { interpolate-size: allow-keywords; }
    details::details-content {
      block-size: 0;
      overflow: hidden;
      transition:
        block-size 280ms var(--wmpg-snap),
        content-visibility 280ms allow-discrete;
    }
    details[open]::details-content { block-size: auto; }
  }
}

/* --- 7. Section eyebrow icons --------------------------------------------
   The 13px line drawing build.mjs (`injectEyebrowIcons`) puts in front of
   every section eyebrow. `currentColor` everywhere, so each icon is already
   whichever of the two eyebrow colours the design chose — no palette here.

   Layout first: the eyebrow is sometimes a plain block and sometimes a flex
   item, so the icon is sized in absolute pixels and nudged onto the mono
   baseline rather than relying on either. The optical alignment is -2px, not
   the -1.5px a 13px box on 11px type would suggest, because the icons are
   drawn with 1.3px strokes inset from the viewBox edge. */
.wmpg-ico {
  width: 13px;
  height: 13px;
  flex: none;
  vertical-align: -2px;
  margin-right: 8px;
  opacity: 0.85;
  overflow: visible;
}

@media (prefers-reduced-motion: no-preference) {
  /* Three of the ten describe something that is, in the product, moving. Those
     keep moving; a fourth kind of motion would be decoration. */

  /* The radio link: each arc lights as the packet reaches it, then decays,
     0.4s apart — outward, the way the signal actually goes. */
  .wmpg-ico--signal .wmpg-arc { animation: wmpg-arc 2.8s ease-out infinite; }
  .wmpg-ico--signal .wmpg-arc + .wmpg-arc { animation-delay: 0.4s; }

  /* The handwheel turns at roughly the rate a hand turns one — one revolution
     every 16s reads as "in use", where anything faster reads as a spinner. */
  .wmpg-ico--wheel .wmpg-spin {
    transform-origin: 8px 8px;
    animation: wmpg-spin 16s linear infinite;
  }

  /* The spindle jogs, which is the operation the pendant commands. */
  .wmpg-ico--jog, .wmpg-ico--machine .wmpg-jog {
    animation: wmpg-jog 3.6s ease-in-out infinite;
  }

  @keyframes wmpg-arc {
    0%        { opacity: 0.12; }
    22%       { opacity: 1; }
    60%, 100% { opacity: 0.12; }
  }
  @keyframes wmpg-jog {
    0%, 100% { transform: translateY(0); }
    50%      { transform: translateY(2.6px); }
  }

  @supports (animation-timeline: view()) {
    /* The other seven draw themselves as their section arrives — the same
       scroll range as the eyebrow they sit in, so line and letters land
       together. Every shape carries `pathLength="1"`, which normalises a
       circle, a trapezoid and a three-segment polyline to the same nominal
       length: one dashoffset animation then draws all of them at one rate,
       with no per-shape measurement.

       Filled shapes (the transmitter dot, the connector pins) have no stroke
       to draw, so they fade instead — `:not([pathLength])` is exactly that
       set, since only the stroked shapes are given the attribute.

       The signal icon's two arcs are excluded because they are already
       animating: one element cannot run a scroll-driven draw and a looping
       pulse from two separate rules, and between the two the pulse is the one
       that says something. Without the `:not()` this rule simply won out on
       source order and the arcs stopped moving at all. */
    .wmpg-ico [pathLength]:not(.wmpg-arc) {
      stroke-dasharray: 1;
      stroke-dashoffset: 1;
      animation: wmpg-draw linear both;
      animation-timeline: view();
      animation-range: cover 3% cover 21%;
    }
    .wmpg-ico circle:not([pathLength]) {
      animation: wmpg-ink linear both;
      animation-timeline: view();
      animation-range: cover 8% cover 22%;
    }
    @keyframes wmpg-draw { to { stroke-dashoffset: 0; } }
    @keyframes wmpg-ink { from { opacity: 0; } to { opacity: 1; } }
  }
}

/* --- 8. Hero, and the arrow on a text link -------------------------------
   The home hero is the one screen that has to carry the brand on its own, and
   it is the screen a visitor judges "static" from. It already had two pieces
   of motion the design drew — the rising entrance and a 4.5s ring leaving the
   product — and section 6 added the parallax and the two dashed detent rings.
   What follows finishes the idea: the ring is a transmitter, so it transmits
   more than once, and the light behind the product is not a fixed gradient. */

@media (prefers-reduced-motion: no-preference) {
  /* A second emission, half a cycle behind the design's own. One ring reads as
     a decorative halo; two, evenly spaced, read as a link that keeps sending —
     which is the claim the page makes in words directly beside it.

     Its own keyframes rather than the design's `wmpg-pulse`: that name is
     declared in Home's helmet and exists on no other page, and a ring whose
     animation-name does not resolve does not disappear, it sits still at full
     opacity. Centred with `inset:0;margin:auto` rather than a translate,
     because `transform` is what the animation is using. */
  @keyframes wmpg-emit {
    0%   { transform: scale(0.6); opacity: 0.5; }
    100% { transform: scale(2.2); opacity: 0; }
  }
  #top [style*='aspect-ratio:1/1.06']::before {
    content: '';
    position: absolute;
    inset: 0;
    margin: auto;
    width: 56%;
    height: auto;
    aspect-ratio: 1;
    border: 1px solid rgba(227, 6, 19, 0.18);
    border-radius: 50%;
    pointer-events: none;
    animation: wmpg-emit 4.5s ease-out infinite;
    animation-delay: 2.25s;
  }

  /* The hero's key light. The design paints it as a fixed radial wash on a
     full-bleed overlay div; breathing it on a 9s cycle is the difference
     between a rendered image and a room with something running in it. The
     overlay is already `position:absolute;inset:0` and already sits below the
     content in tree order, so it is the one element here that can be scaled
     without touching anything's position. */
  @keyframes wmpg-breathe {
    0%, 100% { opacity: 0.75; transform: scale(1); }
    50%      { opacity: 1;    transform: scale(1.06); }
  }
  #top > [style*='position:absolute'][style*='inset:0'] {
    animation: wmpg-breathe 9s ease-in-out infinite;
    will-change: opacity, transform;
  }
}

/* The arrow build.mjs (`injectLinkArrows`) split out of the link text. It
   moves 3px, which at 15px type is exactly one stroke width — enough to
   register as a response, too little to reflow the sentence. `inline-block`
   is what makes it transformable at all. */
.wmpg-arrow {
  display: inline-block;
  transition: transform 180ms var(--wmpg-snap);
}
@media (hover: hover) {
  a:hover .wmpg-arrow { transform: translateX(3px); }
}
a:active .wmpg-arrow { transform: translateX(1px); transition-duration: 60ms; }
