/* AEROSENS — shared components in the PCB-silkscreen world: nav, footer,
   pads (buttons), modules (component blocks), traces, test points, title
   blocks, logs. Built on tokens from base.css. */

/* Skip link — first focusable element on every page. */
.skip-link {
  position: fixed;
  left: var(--space-4);
  top: -100%;
  z-index: 30;
  padding: var(--space-3) var(--space-6);
  background: var(--signal-red);
  color: #ffffff;
  font-family: var(--font-data);
  font-size: 0.8rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  border-radius: var(--radius);
}

.skip-link:focus {
  top: var(--space-4);
}

/* ---- Return-to-top pad --------------------------------------------------- */
/* A pad, not a floating action button. The round FAB every other site uses is
   unavailable here: 50% radius means contact geometry (The Round-Means-Contact
   Rule), and a resting shadow is banned outright — so this is an outline pad
   in the buttons' own grammar, 2px radius, mono uppercase, flat, marking in
   the accent so it turns navy on the Defense sheet for free. The chevron is a
   routed corner: two 45° runs and a miter join, the same turn a trace makes.

   No JS is required for it to work. `href="#top"` scrolls a document to its
   start even with no element of that id (the fragment name is special-cased),
   and base.css already sets `scroll-behavior: smooth` on html with a
   reduced-motion override — so the scroll is smooth, honours the preference,
   and needs no script to animate it.

   Which is why the resting state here is the finished, visible control (The
   Energized-Default Rule): with no JS the pad simply sits there and works.
   to-top.js marks the root `.to-top-js`, and that class — not the base rule —
   is what switches on hide-until-scrolled. */
.to-top {
  position: fixed;
  right: var(--space-4);
  bottom: var(--space-4);
  z-index: 10; /* under the sticky header (20) so an open nav panel wins */
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  min-height: 38px;
  padding: 0.4rem 0.8rem;
  border: 1.5px solid var(--ink);
  border-radius: var(--radius-sm);
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-data);
  font-weight: 500;
  font-size: 0.72rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  text-decoration: none;
  transition: background-color 0.15s ease, border-color 0.15s ease,
    color 0.15s ease;
}

.to-top:hover,
.to-top:focus-visible {
  border-color: var(--accent-dark);
  color: var(--accent-dark);
  background: var(--accent-tint);
  text-decoration: none;
}

.to-top svg {
  display: block;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linejoin: miter;
}

/* The script can only mark the root after the pad has already been laid out in
   its visible resting state, so without this the pad *fades out* on every load.
   `.to-top-boot` is dropped a frame later; two root classes out-specify one, so
   this needs no !important. */
.to-top-boot.to-top-js .to-top {
  transition: none;
}

/* Hide-until-scrolled, only once the script has said it is in charge. The rise
   is the power-up, at the nav's 8px — not a new motion idea. `visibility` is
   what keeps the hidden pad out of the tab order; its transition is delayed by
   the fade's own duration on the way out and immediate on the way in, so the
   pad is never both invisible and focusable. */
.to-top-js .to-top {
  opacity: 0;
  visibility: hidden;
  transform: translateY(8px);
  transition: opacity 0.3s cubic-bezier(0.19, 1, 0.22, 1),
    transform 0.3s cubic-bezier(0.19, 1, 0.22, 1), visibility 0s linear 0.3s,
    background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}

.to-top-js .to-top.is-shown {
  opacity: 1;
  visibility: visible;
  transform: none;
  transition: opacity 0.3s cubic-bezier(0.19, 1, 0.22, 1),
    transform 0.3s cubic-bezier(0.19, 1, 0.22, 1), visibility 0s linear 0s,
    background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}

/* ---- Site header — the board's top edge ------------------------------- */
.site-header {
  position: sticky;
  top: 0;
  z-index: 20;
  background: var(--paper);
  border-bottom: 1px solid var(--hairline);
}

/* The board-edge rail: a thin ink line with the copper claim strip under it,
   reading as the routed edge of the board rather than a decorative bar. */
.site-header::before {
  content: "";
  display: block;
  height: 1px;
  background: var(--ink);
}

.nav-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  /* Horizontal padding restated here because this shorthand would otherwise
     zero out .container's side padding on the same element, leaving the nav
     flush with the screen edge on phones. */
  padding: var(--space-3) var(--space-6);
  gap: var(--space-4);
  min-height: 64px;
}

.nav-brand {
  position: relative;
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  gap: 0.6rem;
}

.nav-brand:hover,
.nav-brand:focus-visible {
  text-decoration: none;
}

.wordmark-img {
  display: block;
  width: auto;
  height: 1.65rem;
}

/* The nav mark scales with the sheet. The header's width budget is the one
   place on the site where the wordmark competes for room — with the switch on
   phones and with six nav labels just above the collapse breakpoint — and it
   is the only element there that can give. Scoped to .nav-brand so the footer
   mark keeps its own fixed size. */
.nav-brand .wordmark-img {
  height: clamp(1.25rem, 0.8rem + 1.35vw, 1.65rem);
  /* The mark scales, it never distorts: as a flex item under pressure the img
     would shrink on the width axis alone and squash the lettering. The LED
     beside it is what gives first. */
  flex: none;
}

/* PWR LED beside the wordmark — the one always-on live indicator. */
.pwr-led {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--signal-red);
  box-shadow: 0 0 0 3px rgba(var(--signal-red-rgb), 0.15);
  animation: led-pulse 2.6s ease-in-out infinite;
}

/* The shared live-dot pulse. Slow ease-in-out keeps it reading as a lit light
   breathing, not a strobe — the smoothness (not the floor) is what keeps
   activity/fault blinking off the table, so the range can run deeper than
   the old 0.72 floor without losing that. All three live dots share it. */
@keyframes led-pulse {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0.4;
  }
}

.nav-links {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  list-style: none;
  margin: 0;
  padding: 0;
}

.nav-links a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 0 var(--space-3);
  color: var(--ink-2);
  font-family: var(--font-data);
  font-size: 0.78rem;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  /* A nav label is one marking. "Commercial Aviation" and "Defense &
     Government" wrapped to two lines just above the collapse breakpoint, which
     grew the header and left the row looking scrunched. */
  white-space: nowrap;
}

.nav-links a:hover,
.nav-links a:focus-visible {
  color: var(--accent-dark);
  text-decoration: none;
}

/* The populated position: current page reads as a fitted component. */
.nav-links a[aria-current="page"] {
  color: var(--ink);
  box-shadow: inset 0 -2px 0 0 var(--ink);
}

/* The Contact pad keeps its own ink when it is the current page — the
   attribute selector above would otherwise out-specify .btn-primary (the
   CLAUDE.md specificity trap). Current state reads as the darkened pad. */
.nav-links a.btn-primary[aria-current="page"] {
  color: var(--accent-ink);
  background: var(--accent-dark);
  border-color: var(--accent-dark);
  box-shadow: none;
}

.nav-links .btn {
  margin-left: var(--space-2);
}

.nav-links .btn-primary,
.nav-links .btn-primary:hover,
.nav-links .btn-primary:focus-visible {
  color: var(--accent-ink);
}

/* SW1 — the mobile menu switch. The board convention that already means
   "toggle": a two-position slide switch, drawn at fab scale. The pad's own
   1.5px ink outline is the switch body, ::before is the actuator, and the
   legend sits between the two seats it travels between. Geometry derives from
   the four --sw-* inputs, so the narrow-viewport tier restates only those. */
.nav-toggle {
  --sw-w: 92px;
  --sw-actuator: 11px;
  --sw-inset: 4px;
  --sw-seat-gap: 6px;
  /* Travel = padding-box width (--sw-w less the two 1.5px borders) minus the
     actuator and the seat inset it leaves behind at each end. */
  --sw-travel: calc(
    var(--sw-w) - 3px - var(--sw-actuator) - var(--sw-inset) * 2
  );
  position: relative;
  display: none;
  align-items: center;
  justify-content: center;
  /* The footprint is fixed, not negotiable: --sw-travel is derived from
     --sw-w, so a flex-shrunk pad would throw the actuator past its own
     border. The header absorbs a tight budget elsewhere (see the 360px tier). */
  flex: none;
  width: var(--sw-w);
  background: none;
  color: var(--ink);
  border: 1.5px solid var(--ink);
  border-radius: var(--radius-sm);
  min-height: 44px;
  padding: 0 calc(var(--sw-inset) + var(--sw-actuator) + var(--sw-seat-gap));
  font-family: var(--font-data);
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  cursor: pointer;
  transition: border-color 0.15s ease, color 0.15s ease,
    background-color 0.15s ease;
}

/* The actuator: an ink block with reversed-out paper knurl ticks (linework, a
   hard-stopped repeating gradient — no tonal ramp), seated at the OFF end. */
.nav-toggle::before {
  content: "";
  position: absolute;
  top: 6px;
  bottom: 6px;
  left: var(--sw-inset);
  width: var(--sw-actuator);
  border-radius: 1px;
  background-color: var(--ink);
  background-image: repeating-linear-gradient(
    to bottom,
    var(--paper) 0 1px,
    transparent 1px 4px
  );
  background-size: 100% 9px;
  background-repeat: no-repeat;
  background-position: center;
  /* A throw is a state change, not a reveal, so it takes the system's state
     easing rather than reveal.js's exponential curve — the eased middle is
     what makes 66px of travel read as a slide instead of a jump. The ink
     switches with no duration, delayed to the moment the actuator seats: the
     circuit closes on contact, and interpolating ink→accent would ramp through
     a maroon that is on no plate in this system. */
  transition: transform 0.2s ease, background-color 0s linear 0.16s;
}

/* base.css zeroes transition-duration under reduced motion but cannot touch a
   delay, which would leave the seated actuator ink for 160ms before it lit —
   residual animation in the one path that is meant to have none. */
@media (prefers-reduced-motion: reduce) {
  .nav-toggle::before {
    transition-delay: 0s;
  }
}

/* Thrown, or reached by keyboard: the whole pad reads live. The switch is the
   one control on the sheet whose state the visitor needs at a glance, so the
   accent is spent on the state, not on the resting control. */
.nav-toggle:focus-visible,
.nav-toggle[aria-expanded="true"] {
  border-color: var(--accent-dark);
  color: var(--accent-dark);
  background: var(--accent-tint);
}

/* Hover only where a pointer can genuinely hover. On touch, :hover latches
   after a tap and the pad stays lit long after the panel has closed — which
   made the accent read as the switch's resting color rather than its state. */
@media (hover: hover) {
  .nav-toggle:hover {
    border-color: var(--accent-dark);
    color: var(--accent-dark);
    background: var(--accent-tint);
  }
}

/* The actuator lands in the ON seat and lights to the accent — the same
   "powered" signal the PWR LED and the test points carry. */
.nav-toggle[aria-expanded="true"]::before {
  transform: translateX(var(--sw-travel));
  background-color: var(--accent);
}

/* The press unseats it: the throw starts under the finger and completes on
   release. Ordered after the state rule so the open pad nudges the other way. */
.nav-toggle:active::before {
  transform: translateX(calc(var(--sw-travel) * 0.16));
}

.nav-toggle[aria-expanded="true"]:active::before {
  transform: translateX(calc(var(--sw-travel) * 0.84));
}

/* 960px, not 880px: six nav labels are a hard 680px of content, so a one-row
   desktop nav needs ~946px before the wordmark starts paying for it. Below
   880px the two long labels used to wrap to a second line and grow the header.
   The noscript fallback in every page's <head> mirrors this number — change
   one, change both. */
@media (max-width: 960px) {
  .nav-toggle {
    display: inline-flex;
  }
  .nav-links {
    display: none;
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    background: var(--paper);
    border-bottom: 1px solid var(--hairline);
    padding: var(--space-2) var(--space-6) var(--space-4);
  }
  /* Throwing the switch closes the circuit, so the panel powers up: the same
     rise-and-fade reveal.js runs, at panel speed. Keyframe-only `from` with no
     fill mode, so the resting state stays the finished, energized panel. */
  .nav-links.is-open {
    display: flex;
    animation: nav-powerup 0.26s cubic-bezier(0.19, 1, 0.22, 1);
  }
  .nav-links li {
    margin: 0;
  }
  .nav-links a {
    width: 100%;
    min-height: 48px;
    padding: 0 var(--space-2);
    border-bottom: 1px solid var(--hairline);
  }
  .nav-links li:last-child a {
    border-bottom: none;
  }
  .nav-links a[aria-current="page"] {
    box-shadow: inset 3px 0 0 0 var(--ink);
    padding-left: var(--space-3);
  }
  .nav-links a.btn-primary[aria-current="page"] {
    box-shadow: none;
    padding-left: var(--space-4);
  }
  .nav-links .btn {
    margin: var(--space-3) 0 0;
    justify-content: center;
  }
  .nav-bar {
    position: relative;
  }
}

@keyframes nav-powerup {
  from {
    opacity: 0;
    transform: translateY(8px);
  }
}


/* ---- Sheet index — the drawing's index, part of the document header ------ */
/* Sits at the foot of a long sheet's hero, anchored under a drawing-weight
   rule: a labelled, numbered list of the sheet's zones. */
.sheet-index {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-2) var(--space-6);
  margin-top: var(--space-12);
  border-top: 1.5px solid var(--ink);
  padding-top: var(--space-3);
}

.sheet-index > .silk-label {
  min-height: 44px;
  align-items: center;
  display: inline-flex;
}

.sheet-index ul {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0 var(--space-6);
  list-style: none;
  margin: 0;
  padding: 0;
}

.sheet-index li {
  margin: 0;
}

.sheet-index a {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  min-height: 44px;
  font-family: var(--font-data);
  font-size: 0.74rem;
  font-weight: 500;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--ink);
}

/* Inherits the parent's mono 500. It used to add font-weight: 600, which had no
   real face and so only ever rendered as a faked bold; the accent colour already
   carries the emphasis, so the index number leans on that instead. */
.sheet-index a .idx {
  color: var(--accent-dark);
}

.sheet-index a:hover,
.sheet-index a:focus-visible {
  color: var(--accent-dark);
  text-decoration: none;
  box-shadow: inset 0 -2px 0 0 var(--accent-dark);
}

/* Quiet return link at the foot of a long sheet. */
.back-to-index {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  font-family: var(--font-data);
  font-size: 0.72rem;
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

/* Pinout — a capability section drawn as the component's pin functions:
   a wide IC banner with numbered pins, then the pin-function rows grouped
   into banks. Pin numbers echo the .steps counter voice in pad form. */
.pinout .board-svg {
  margin-bottom: var(--space-4);
}

/* The wide IC banner is drawn on a 1120-unit viewBox. Below 880px it scales to
   ~0.3, which puts its silk marking at ~3.4px — unreadable, unzoomable lint
   above content that works. Every identifier it carries (the pin numbers, the
   part name, the bank grouping) is repeated in the .pin-row list underneath at
   full size, so the banner is the one part of this signature that does not
   survive the width. Drop it rather than ship it illegible; the pinout itself
   still reads. Restoring it needs larger authored type in the SVG, not CSS. */
@media (max-width: 880px) {
  .pinout > .board-svg {
    display: none;
  }
}

.pin-bank {
  margin-top: var(--space-10);
}

.pin-bank > .silk-label {
  margin-bottom: var(--space-4);
}

.pin-bank .grid-2 {
  gap: var(--space-6) var(--space-8);
}

.pin-row {
  display: grid;
  grid-template-columns: 44px 1fr;
  gap: var(--space-4);
  align-items: start;
}

.pin-num {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  margin-top: 2px;
  border: 1.5px solid var(--signal-navy);
  border-radius: var(--radius-sm);
  font-family: var(--font-data);
  font-variant-numeric: tabular-nums;
  font-size: 0.95rem;
  font-weight: 500;
  color: var(--signal-navy);
  transition: background-color 0.15s ease, color 0.15s ease;
}

.pin-row:hover .pin-num {
  background: var(--signal-navy);
  color: #ffffff;
}

/* Hovering a pin-function row lights the same pin on the IC banner above:
   the pad and its number grow slightly out from the chip edge and the number
   takes the navy of the filled .pin-num box. Rows and pins are paired by
   matching data-pin values; pure CSS via :has(), so with the banner hidden
   (mobile) or :has() unsupported it simply does nothing. */
.pin-mark {
  transform-box: fill-box;
  transform-origin: 50% 100%;
  transition: transform 0.15s ease;
}

.pinout:has(.pin-row[data-pin="01"]:hover) .pin-mark[data-pin="01"],
.pinout:has(.pin-row[data-pin="02"]:hover) .pin-mark[data-pin="02"],
.pinout:has(.pin-row[data-pin="03"]:hover) .pin-mark[data-pin="03"],
.pinout:has(.pin-row[data-pin="04"]:hover) .pin-mark[data-pin="04"],
.pinout:has(.pin-row[data-pin="05"]:hover) .pin-mark[data-pin="05"],
.pinout:has(.pin-row[data-pin="06"]:hover) .pin-mark[data-pin="06"],
.pinout:has(.pin-row[data-pin="07"]:hover) .pin-mark[data-pin="07"],
.pinout:has(.pin-row[data-pin="08"]:hover) .pin-mark[data-pin="08"],
.pinout:has(.pin-row[data-pin="09"]:hover) .pin-mark[data-pin="09"],
.pinout:has(.pin-row[data-pin="10"]:hover) .pin-mark[data-pin="10"] {
  transform: scale(1.25);
}

.pinout:has(.pin-row[data-pin="01"]:hover) .pin-mark[data-pin="01"] text,
.pinout:has(.pin-row[data-pin="02"]:hover) .pin-mark[data-pin="02"] text,
.pinout:has(.pin-row[data-pin="03"]:hover) .pin-mark[data-pin="03"] text,
.pinout:has(.pin-row[data-pin="04"]:hover) .pin-mark[data-pin="04"] text,
.pinout:has(.pin-row[data-pin="05"]:hover) .pin-mark[data-pin="05"] text,
.pinout:has(.pin-row[data-pin="06"]:hover) .pin-mark[data-pin="06"] text,
.pinout:has(.pin-row[data-pin="07"]:hover) .pin-mark[data-pin="07"] text,
.pinout:has(.pin-row[data-pin="08"]:hover) .pin-mark[data-pin="08"] text,
.pinout:has(.pin-row[data-pin="09"]:hover) .pin-mark[data-pin="09"] text,
.pinout:has(.pin-row[data-pin="10"]:hover) .pin-mark[data-pin="10"] text {
  fill: var(--signal-navy);
}

.pin-row h3 {
  font-size: 1rem;
  margin-bottom: var(--space-1);
}

.pin-row p {
  font-size: 0.94rem;
  margin: 0;
}

/* ---- Silk labels — functional markings, never kickers ------------------ */
/* A net name, title-block entry, or figure caption. Placed on frames,
   rules, and meta rows; it labels a structure, it does not decorate a
   heading. */
.silk-label {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  font-family: var(--font-data);
  font-size: 0.72rem;
  font-weight: 500;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: var(--ink-2);
}

.silk-label.claim {
  color: var(--signal-red-dark);
}

.silk-label.navy {
  color: var(--signal-navy);
}

/* Net rule — a section's labelled boundary: hairline with an origin pad. */
.net-rule {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-bottom: var(--space-8);
}

.net-rule::before {
  content: "";
  flex: none;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  border: 2px solid var(--ink);
  background: var(--paper);
}

.net-rule::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--hairline);
}

/* ---- Buttons — pads ----------------------------------------------------- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  min-height: 46px;
  padding: 0.65rem 1.4rem;
  border-radius: var(--radius-sm);
  font-family: var(--font-data);
  font-weight: 500;
  font-size: 0.84rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  border: 1.5px solid transparent;
  cursor: pointer;
  transition: background-color 0.15s ease, border-color 0.15s ease,
    color 0.15s ease;
}

.btn:hover,
.btn:focus-visible {
  text-decoration: none;
}

.btn-primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-ink);
}

.btn-primary:hover,
.btn-primary:focus-visible {
  background: var(--accent-dark);
  border-color: var(--accent-dark);
  color: var(--accent-ink);
}

.btn-outline {
  background: transparent;
  border-color: var(--ink);
  color: var(--ink);
}

.btn-outline:hover,
.btn-outline:focus-visible {
  border-color: var(--accent-dark);
  color: var(--accent-dark);
  background: var(--accent-tint);
}

/* Button rows wrap with real gaps. */
p:has(> .btn) {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
}

.text-center p:has(> .btn) {
  justify-content: center;
}

/* `p:has(> .btn)` turns the paragraph into a flex row, so `text-align` on the
   paragraph itself no longer positions the button — it needs the flex
   property. Without this, `<p class="text-center">` around a lone button
   silently renders flush left (index.html "See press coverage"). */
p.text-center:has(> .btn) {
  justify-content: center;
}

/* ---- Fab frame — ruled drawing frame with corner fiducials -------------- */
/* The world's signature framing for featured media and diagrams: a ruled
   border with fiducial crosshairs at two opposite corners. */
.fab-frame {
  position: relative;
  border: 1.5px solid var(--ink);
  border-radius: var(--radius);
  background: var(--paper);
}

.fab-frame > img,
.fab-frame > .video {
  border-radius: 1px;
}

/* Exposed variant — for cutout art (transparent-background PNGs) that should
   read as loose on the page rather than boxed: keeps the corner fiducials
   but drops the ruled border and paper fill, so the section's own background
   shows through the art's transparent pixels instead of a whiter seam. */
.fab-frame--exposed {
  border: none;
  background: none;
}

/* Art slot — centers cutout art at reduced scale within its frame, so it
   reads as an object placed on the page rather than a photo filling it. */
.art-slot {
  display: grid;
  place-items: center;
}

.art-slot img {
  width: 50%;
  height: auto;
}

/* Inner sleeve for the hero board drawing inside the fab frame. Its min-width
   guard (plus the .hero-grid one below) keeps the drawing from sizing its grid
   track wide and dragging the page into horizontal scroll; the drawing itself
   scales down to the phone width. Fit-over-scroll is the owner's call
   (2026-08-14) — the fine silk shrinks on small screens and pinch-zoom is the
   recourse, rather than turning the hero into a scroll box. */
.board-scroll {
  min-width: 0;
}

@media (max-width: 880px) {
  .hero-grid > * {
    min-width: 0;
  }
}

.fiducial {
  position: absolute;
  width: 18px;
  height: 18px;
  pointer-events: none;
  color: var(--ink);
}

.fiducial::before {
  content: "";
  position: absolute;
  inset: 4px;
  border: 1.5px solid currentColor;
  border-radius: 50%;
}

.fiducial::after {
  content: "";
  position: absolute;
  inset: 0;
  background:
    linear-gradient(currentColor, currentColor) center / 1.5px 100% no-repeat,
    linear-gradient(currentColor, currentColor) center / 100% 1.5px no-repeat;
}

.fiducial.tl {
  top: -9px;
  left: -9px;
}

.fiducial.br {
  bottom: -9px;
  right: -9px;
}

/* ---- Hero ---------------------------------------------------------------- */
.hero {
  position: relative;
  overflow: hidden;
  isolation: isolate;
  padding: var(--space-16) 0;
  border-bottom: 1px solid var(--hairline);
}

/* ---- Signal field — the sheet's own broadcast ------------------------------ */
/* The RF test point's rings (.tp-rings, TP1 on the home board) promoted from
   one point on one diagram to the ground of every sheet. Same device, same
   grammar: a ring leaves an emitter, expands, and fades, staggered so the
   field never empties. This is literally what the product does — a 2.4 GHz
   BLE advertisement leaving a tag — drawn at page scale, which makes it the
   existing motion idea at a new size rather than a second one.

   Marks in the track's own accent, so the Defense sheet broadcasts navy and
   every other sheet crimson, with no extra rule. Alpha is deliberately near
   the floor of visible: this is ground texture, not an element.

   Budget: two composited layers per page, transform and opacity only, one
   region per sheet, paused while offscreen (see reveal.js). */
.hero::before {
  content: "";
  position: absolute;
  z-index: 0;
  top: 44%;
  left: 80%;
  width: min(150vw, 1400px);
  height: min(150vw, 1400px);
  border-radius: 50%;
  pointer-events: none;
  /* Hard-stop rings: 1px of ink, 61px of nothing. Linework, not a tonal ramp,
     so the Flat Board Rule holds — the same way .board-grid draws its lattice
     and .module.ic draws its pin ticks. */
  background: repeating-radial-gradient(
    circle at center,
    rgba(var(--accent-rgb), 0.3) 0 1px,
    transparent 1px 62px
  );
  /* The field has to end somewhere. A mask fades the linework out rather than
     cutting it at a hard circular edge, which would read as a drawn boundary
     the drawing never intended. It fades marks, it does not paint a ramp. */
  -webkit-mask-image: radial-gradient(
    circle at center,
    #000 6%,
    rgba(0, 0, 0, 0.5) 42%,
    transparent 70%
  );
  mask-image: radial-gradient(
    circle at center,
    #000 6%,
    rgba(0, 0, 0, 0.5) 42%,
    transparent 70%
  );
  transform: translate(-50%, -50%) scale(0.5);
  animation: signal-wave-solo 16s linear infinite;
}

/* The hero's wave runs alone, so it carries the whole cycle and never fades to
   nothing — it floors at 0.25 instead of 0. At that floor the restart is
   invisible: scale returns 1 → 0.5, which does not move any ring, it only
   brings back the intermediate ones the wave had spread past, and they arrive
   at 0.25 × 0.3 alpha. The scattered emitters keep the plain signal-wave
   envelope: they are occasional weather and are meant to come and go. */
@keyframes signal-wave-solo {
  0% {
    transform: translate(-50%, -50%) scale(0.5);
    opacity: 0.25;
  }
  30%,
  70% {
    opacity: 1;
  }
  100% {
    transform: translate(-50%, -50%) scale(1);
    opacity: 0.25;
  }
}

/* One wavefront, and only one. A second layer offset half a cycle used to run
   here so the field never emptied, but the two layers sit on the same origin
   at different scales, so their ring pitches differ (62px × scale) and they
   read as two interleaved ring systems — a second emitter, which is exactly
   what The One Broadcast Rule forbids. The dip at the loop point is the
   honest cost of one wave, and it is what a single advertisement looks like:
   it leaves, it expands, it fades, the next one leaves. */

/* Scale runs 0.5 to 1 — one ring period — and the opacity floor at both ends
   hides the restart, where the ring set doubles back to its finer spacing. */
@keyframes signal-wave {
  0% {
    transform: translate(-50%, -50%) scale(0.5);
    opacity: 0;
  }
  22%,
  62% {
    opacity: 1;
  }
  100% {
    transform: translate(-50%, -50%) scale(1);
    opacity: 0;
  }
}

/* Content rides above the field. */
.hero > .container {
  position: relative;
  z-index: 1;
}

/* Stop broadcasting while the visitor cannot see it. reveal.js toggles this. */
.hero.signal-idle::before {
  animation-play-state: paused;
}

/* Scattered emitters — the same broadcast, seeded by reveal.js onto one
   plane behind the whole sheet rather than into individual sections, so a
   wavefront that overlaps a section border simply keeps going: across the
   hairline, beneath the substrate veil of an .alt panel, back out the other
   side. Decoration only, and JS-only on purpose: it rides reveal.js's early
   returns, so no-JS and reduced-motion visitors get the hero's field (or its
   static ring) and nothing else — nothing is ever missing, there is simply
   less weather. Placement, phase, and period arrive as custom properties
   randomized per visit; the randomized negative delay is what makes the
   field read as scattered rather than synchronized. */
main {
  position: relative;
  isolation: isolate; /* keeps the z:-1 plane above the body ground */
}

.signal-layer {
  position: absolute;
  inset: 0;
  z-index: -1; /* beneath every section fill, module, and line of copy */
  overflow: clip;
  pointer-events: none;
}

.signal-emitter {
  position: absolute;
  top: var(--sf-y, 50%);
  left: var(--sf-x, 85%);
  width: min(120vw, var(--sf-size, 1100px));
  height: min(120vw, var(--sf-size, 1100px));
  border-radius: 50%;
  background: repeating-radial-gradient(
    circle at center,
    rgba(var(--accent-rgb), 0.26) 0 1px,
    transparent 1px 62px
  );
  -webkit-mask-image: radial-gradient(
    circle at center,
    #000 6%,
    rgba(0, 0, 0, 0.5) 42%,
    transparent 70%
  );
  mask-image: radial-gradient(
    circle at center,
    #000 6%,
    rgba(0, 0, 0, 0.5) 42%,
    transparent 70%
  );
  transform: translate(-50%, -50%) scale(0.5);
  opacity: 0;
  animation: signal-wave var(--sf-dur, 18s) linear infinite;
  animation-delay: var(--sf-delay, 0s);
}

.signal-emitter.signal-idle {
  animation-play-state: paused;
}

/* Reduced motion keeps the field and loses the travel: the ring set drawn at
   rest, mid-expansion. The Energized-Default Rule applies to ambient layers
   too — the fallback is the board still drawn, not a blank sheet. */
@media (prefers-reduced-motion: reduce) {
  .hero::before {
    animation: none;
    opacity: 1;
    transform: translate(-50%, -50%) scale(0.75);
  }
}

.hero-grid {
  display: grid;
  gap: var(--space-12);
  align-items: center;
}

@media (min-width: 880px) {
  .hero-grid {
    grid-template-columns: 1.05fr 0.95fr;
  }
}

/* Title strip above the H1: the page's drawing header, carrying the document
   class on a ruled line. The sheet address that used to lead this strip was
   dropped (2026-08-14) — its "SHT" abbreviation misread badly. The footer
   title block still carries the sheet number, spelled out. */
.title-strip {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-4);
  padding-bottom: var(--space-3);
  border-bottom: 1px solid var(--hairline);
  margin-bottom: var(--space-6);
}

.hero img {
  border-radius: var(--radius);
}

/* ---- Board diagram (inline SVG) ----------------------------------------- */
.board-svg {
  display: block;
  width: 100%;
  height: auto;
  font-family: var(--font-data);

  /* Diagram type is set at 400 (the default) or 500 via `font-weight="500"` on
     the <text> — the only two IBM Plex Mono files we ship. Do not reach for 600
     or 700 here: there is no such face, so the browser fakes one, and Safari's
     fake is visibly heavier than Chrome's. `body` in base.css turns weight
     synthesis off site-wide, which backstops this; the rule is stated here
     because this is where the temptation to bold a label comes up. */
}

.board-svg .outline {
  fill: none;
  stroke: var(--ink);
  stroke-width: 1.5;
}

.board-svg .silk {
  fill: none;
  stroke: var(--ink);
  stroke-width: 1.5;
}

.board-svg .hair {
  fill: none;
  stroke: var(--hairline);
  stroke-width: 1;
}

.board-svg text {
  fill: var(--ink);
}

.board-svg .t2 {
  fill: var(--ink-2);
}

/* Type set on a filled pad. This has to be a class: `.board-svg text` above is
   a CSS rule, and a presentation attribute (fill="#ffffff") carries no
   specificity at all, so an inline white fill silently loses to the ink and
   the label reads near-black on navy. */
.board-svg text.on-pad {
  fill: var(--paper);
}

.board-svg .trace {
  fill: none;
  stroke-width: 2;
  stroke-linejoin: miter;
  stroke-linecap: round;
}

.board-svg .trace.claim {
  stroke: var(--signal-red);
}

.board-svg .trace.navy {
  stroke: var(--signal-navy);
}

.board-svg .pad {
  fill: var(--ink);
}

.board-svg .pad.claim {
  fill: var(--signal-red);
}

.board-svg .pad.navy {
  fill: var(--signal-navy);
}

.board-svg .via {
  fill: var(--paper);
  stroke: var(--ink);
  stroke-width: 1.5;
}

/* Map figure — land and graticule. Fig. D-5 on the Defense sheet is the one
   drawing on the site carrying real geography: Natural Earth 110m coastlines,
   Mercator-projected into the viewBox and Douglas-Peucker simplified, with the
   ocean leg struck as a true great circle. Land takes the substrate fill so the
   ocean stays paper and no new hue enters the palette; the graticule marks in
   the placement-grid ink, a step lighter than the hairline coastline so the
   two never compete for the same read. */
.board-svg .land {
  fill: var(--substrate);
  stroke: var(--hairline);
  stroke-width: 1;
}

.board-svg .grat {
  fill: none;
  stroke: var(--grid-dot);
  stroke-width: 1;
}

/* Signal arcs in drawn figures — RF drawn as still concentric strokes. */
.board-svg .sig {
  fill: none;
  stroke: var(--signal-navy);
  stroke-width: 1.5;
  stroke-linecap: round;
  opacity: 0.65;
}

.board-svg .sig.claim {
  stroke: var(--signal-red);
}

.board-svg .dashed {
  stroke-dasharray: 4 5;
}

/* Continuity probe — hover/focus a net group and its whole route answers. */
.board-svg a {
  outline-offset: 4px;
}

/* The connector plate IS the hit target, not just its outline. `.silk` is
   `fill: none`, so a linked block was only answering on its 1.5px stroke and
   its glyphs — roughly a third of the area it draws. Scoped to connectors
   inside a link so U1's body never starts intercepting the traces beneath it. */
.board-svg a .silk.connector {
  pointer-events: all;
}

/* Focus on a net group: the global crimson ring is indistinguishable from
   crimson board artwork, and the global `a:focus-visible` underline stripes
   every text node in the group at once. Mark it in the drawing's own voice —
   a dashed ink outline — and leave the underline to .net-label, which already
   carries one at rest.

   Ring the CONNECTOR PLATE, not the <a>. The link deliberately wraps the whole
   net — trace, via, and plate — so hovering it lights the entire route, which
   also means its bounding box reaches all the way back to the hub. On the two
   boards where the nets fan out closely (commercial's four modules sit 18 units
   apart and their boxes overlap by up to 38), an outline on the <a> drew one
   enormous rectangle that swallowed the neighbouring module and clipped against
   the edge of the viewBox. The plate is what you actually activate, so that is
   what gets ringed; the thickened trace still shows which route is live. */
/* :hover as well as :focus-visible. The global `a:hover` underline in base.css
   inherits into every text node the link contains, so hovering a connector
   struck a line through its designator, its sublabels and its arrow all at once
   — the ruled-out look, not a link. The underline belongs to .net-label alone,
   which sets it on itself and so is unaffected by this reset. */
.board-svg a:hover,
.board-svg a:focus-visible {
  outline: none;
  text-decoration: none;
}

/* 3px, not the 6px used elsewhere: `outline-offset` is CSS pixels, so as the
   board scales down the offset stays fixed while the gap between the plate and
   the edge of the viewBox shrinks with it. Commercial's M1 plate sits 8 viewBox
   units from the top — about 6px at the narrow breakpoint — so a 6px ring fell
   outside the viewport and was clipped away exactly where it mattered. */
.board-svg a:focus-visible .silk.connector {
  outline: 2px dashed var(--ink);
  outline-offset: 3px;
}

/* Static link affordance: net labels carry an underline at rest, so the
   connectors read clickable before any hover. */
.board-svg a .net-label {
  fill: var(--ink);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 4px;
}

.board-svg a:hover .trace,
.board-svg a:focus-visible .trace {
  stroke-width: 3.5;
}

.board-svg a:hover .connector,
.board-svg a:focus-visible .connector {
  stroke-width: 2.5;
}

/* The net label deliberately has NO hover style of its own. It used to jump to
   700, which had no real face behind it (see the .board-svg font-synthesis
   note) — Safari smeared it and the glyph run grew ~4.7% mid-hover. The obvious
   replacement, thickening the underline, is honoured by WebKit but ignored
   outright by Blink on SVG <text>, so it would just re-introduce a Safari/Chrome
   split. The probe already answers identically in both engines through the
   route itself (trace 2->3.5px, connector 1.5->2.5px below), and the label
   carries a rest underline, so nothing further is needed here. */

/* Signal test point — pulsing rings mark the live RF point. */
.board-svg .tp-rings circle {
  fill: none;
  stroke: var(--signal-red);
  opacity: 0.55;
}

.board-svg .tp-rings .r1 {
  animation: ring-pulse 3.2s ease-out infinite;
}

.board-svg .tp-rings .r2 {
  animation: ring-pulse 3.2s ease-out 1.06s infinite;
}

.board-svg .tp-rings .r3 {
  animation: ring-pulse 3.2s ease-out 2.13s infinite;
}

@keyframes ring-pulse {
  0% {
    transform: scale(0.45);
    opacity: 0.7;
  }
  100% {
    transform: scale(1.15);
    opacity: 0;
  }
}

.board-svg .tp-rings circle {
  transform-origin: center;
  transform-box: fill-box;
}

@media (prefers-reduced-motion: reduce) {
  .board-svg .tp-rings circle {
    transform: none;
    opacity: 0.35;
  }
}

/* ---- Modules — placed components (replaces cards) ------------------------ */
.module {
  position: relative;
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  padding: var(--space-6);
  background: var(--paper);
}

.module h3 {
  margin-bottom: var(--space-2);
}

.module p:last-child {
  margin-bottom: 0;
}

/* Reference designator — the component's corner tag. Real information:
   class letter + instance number, unique per page. Set on a paper chip so it
   stays legible over card media. */
.module[data-refdes]::after {
  content: attr(data-refdes);
  position: absolute;
  top: var(--space-2);
  right: var(--space-2);
  z-index: 1;
  font-family: var(--font-data);
  font-size: 0.72rem;
  font-weight: 500;
  letter-spacing: 0.08em;
  color: var(--ink);
  background: var(--paper);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-sm);
  padding: 2px 6px;
}

/* IC variant — pin ticks along the top edge. */
.module.ic {
  border-top: 1.5px solid var(--ink);
}

.module.ic::before {
  content: "";
  position: absolute;
  top: -1px;
  left: var(--space-4);
  right: var(--space-4);
  height: 5px;
  background: repeating-linear-gradient(
    90deg,
    var(--ink) 0,
    var(--ink) 1.5px,
    transparent 1.5px,
    transparent 12px
  );
}

/* Track connectors on the home board — J1 / J2. */
.connector-card {
  position: relative;
  border: 1.5px solid var(--ink);
  border-radius: var(--radius);
  padding: var(--space-8);
  background: var(--paper);
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.connector-card .silk-label {
  margin-bottom: var(--space-1);
}

/* The two product names sit under the sheet's hidden "Two missions" h2, so
   they are h3s in the outline — but they keep the h2 scale the plates were
   drawn at. */
.connector-card h3 {
  font-size: clamp(1.6rem, 3.2vw, 2.2rem);
  line-height: 1.15;
}

/* ---- Plate markings, not kickers ----------------------------------------- */
/* DESIGN.md's connector-card spec calls for a silk-label track name on the
   plate, and permits a silk label to mark a callout block — so the words are
   sanctioned. What was not sanctioned is their SHAPE: a categorical label set
   as a lead-in line directly above the heading is the eyebrow this system
   bans outright, whichever spec allows the text. Move them into the plate's
   top edge, the register the refdes tag already occupies, so each reads as a
   marking ON the part rather than an introduction TO the heading. The heading
   now leads its own card. Applies to exactly four cards sitewide: J1/J2 on the
   home board and S1/P1 on the defense sheet, plus the AeroCMS callout. */
.connector-card > .silk-label:first-child,
.module.ic > .silk-label:first-child,
.callout > .callout-kicker:first-child {
  position: absolute;
  top: var(--space-3);
  left: var(--space-4);
  right: var(--space-12);
  margin: 0;
}

/* Host the marking band: the plate needs room above its heading for the edge
   row, or the label collides with the content it used to sit on top of. */
.connector-card:has(> .silk-label:first-child),
.module.ic:has(> .silk-label:first-child),
.callout:has(> .callout-kicker:first-child) {
  padding-top: var(--space-10);
}

.connector-card[data-refdes]::after {
  content: attr(data-refdes);
  position: absolute;
  top: var(--space-3);
  right: var(--space-4);
  font-family: var(--font-data);
  font-size: 0.72rem;
  font-weight: 500;
  color: var(--ink);
}

/* Edge pads: the connector's contact fingers along the top edge. */
.connector-card::before {
  content: "";
  position: absolute;
  top: -1.5px;
  left: var(--space-6);
  width: 96px;
  height: 4px;
  background: repeating-linear-gradient(
    90deg,
    var(--pad-ink, var(--ink)) 0,
    var(--pad-ink, var(--ink)) 14px,
    transparent 14px,
    transparent 22px
  );
}

.connector-card.commercial {
  --pad-ink: var(--signal-red);
}

.connector-card.defense {
  --pad-ink: var(--signal-navy);
}

.connector-card p {
  max-width: 52ch;
}

.connector-card .btn {
  align-self: flex-start;
  margin-top: auto;
}

/* ---- Test points & measured values --------------------------------------- */
.tp-dot {
  display: inline-block;
  flex: none;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  border: 2px solid var(--signal-red);
  background: var(--paper);
}

.tp-dot.live {
  background: var(--signal-red);
  box-shadow: 0 0 0 3px rgba(var(--signal-red-rgb), 0.15);
  animation: led-pulse 2.6s ease-in-out infinite;
}

/* A measurement: dimension ticks flanking a mono value. Quantities render
   as measured values with units — never decorated prose. */
.measure {
  text-align: center;
}

.measure .measure-value {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-3);
  font-family: var(--font-data);
  font-variant-numeric: tabular-nums;
  font-size: 2.2rem;
  font-weight: 500;
  color: var(--ink);
}

.measure .measure-value::before,
.measure .measure-value::after {
  content: "";
  flex: none;
  width: 26px;
  height: 12px;
  border-bottom: 1.5px solid var(--ink-2);
}

.measure .measure-value::before {
  border-left: 1.5px solid var(--ink-2);
}

.measure .measure-value::after {
  border-right: 1.5px solid var(--ink-2);
}

.measure .measure-label {
  display: block;
  margin-top: var(--space-2);
  color: var(--ink-2);
  font-size: 0.92rem;
}

/* ---- Feature rows (quiet list content) ----------------------------------- */
.feature {
  padding-top: var(--space-4);
  border-top: 1.5px solid var(--ink);
}

.feature h3 {
  margin-bottom: var(--space-2);
}

.feature p:last-child {
  margin-bottom: 0;
}

/* ---- Split sections ------------------------------------------------------- */
.split {
  display: grid;
  gap: var(--space-8);
  align-items: center;
}

@media (min-width: 880px) {
  .split {
    grid-template-columns: 1fr 1fr;
  }
  .split.reverse > :first-child {
    order: 2;
  }
}

.split-media img {
  border-radius: var(--radius);
}

/* Media placeholder — an unpopulated footprint: dashed courtyard outline,
   awaiting its component. */
.media-placeholder {
  aspect-ratio: 4 / 3;
  border-radius: var(--radius);
  border: 1.5px dashed var(--ink-2);
  background: var(--paper);
  display: grid;
  place-items: center;
  text-align: center;
  padding: var(--space-8);
}

/* ---- Drawn figure — an authored board-voice illustration -------------------- */
/* Replaces an unpopulated footprint once the drawing exists: a hairline-framed
   silkscreen illustration with a mono caption row. Reuses .board-svg stroke
   grammar and the .visual-label / .visual-desc caption voices. */
.figure {
  margin: 0;
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  background: var(--paper);
  padding: var(--space-4);
}

.figure .board-svg {
  width: 100%;
  height: auto;
}

/* Below 880px figures scale to the phone width rather than holding a fixed
   drawing width behind a horizontal scroll. Fit-over-scroll is the owner's
   call (2026-08-14): a page of scroll boxes broke the mobile read, so the
   silk markings compress instead and pinch-zoom covers the fine print. */
@media (max-width: 880px) {
  /* A grid/flex item defaults to `min-width: auto`, which sizes its track to
     the widest thing inside it. Without these guards, a wide drawing would
     drag its whole column — and the page — into horizontal scroll. */
  .split > *,
  .split > * > * {
    min-width: 0;
  }
  .figure {
    min-width: 0;
  }
}

.figure-caption {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  margin-top: var(--space-3);
  padding-top: var(--space-3);
  border-top: 1px solid var(--hairline);
}

/* ---- Visual placeholder — an unpopulated footprint ------------------------- */
/* A slot for a drawing / rendering / mockup that doesn't exist yet: dashed
   courtyard outline, a drawn frame glyph, and a description of the intended
   artwork so it can be dropped in later. */
.visual {
  position: relative;
  aspect-ratio: 4 / 3;
  border-radius: var(--radius);
  border: 1.5px dashed var(--ink-2);
  background: var(--paper);
  display: grid;
  place-items: center;
  text-align: center;
  padding: var(--space-6);
}

.visual.wide {
  aspect-ratio: 16 / 9;
}

.visual-inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-3);
  max-width: 46ch;
}

/* A small framed-picture glyph, drawn in CSS in the silkscreen line weight. */
.visual-icon {
  width: 52px;
  height: 42px;
  border: 1.5px solid var(--ink);
  border-radius: var(--radius-sm);
  color: var(--ink);
  position: relative;
  overflow: hidden;
  flex: none;
}

.visual-icon::before {
  content: "";
  position: absolute;
  top: 7px;
  left: 9px;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  border: 1.5px solid currentColor;
}

.visual-icon::after {
  content: "";
  position: absolute;
  left: 6px;
  right: 6px;
  bottom: 6px;
  height: 12px;
  background: repeating-linear-gradient(
    -45deg,
    currentColor 0,
    currentColor 1.5px,
    transparent 1.5px,
    transparent 6px
  );
}

.visual-label {
  font-family: var(--font-data);
  font-size: 0.72rem;
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--accent-dark);
}

.visual-desc {
  font-size: 0.9rem;
  color: var(--ink-2);
  margin: 0;
}

/* A posted slot: the thumbnail is in, the clip is not. The dashed courtyard
   means "nothing placed here yet", so it becomes a solid hairline the moment
   an image lands — and the slot keeps saying, in a corner marking rather than
   across the middle, that the footage is still coming. No play pad: there is
   nothing to play until the clip arrives, and offering one would lie. */
.visual.has-poster {
  border-style: solid;
  border-width: 1px;
  border-color: var(--hairline);
  overflow: hidden;
  padding: 0;
}

.visual-poster {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* The card carries the module name in its own artwork, so the glyph and the
   label step aside and only the status marking stays. */
.visual.has-poster .visual-icon,
.visual.has-poster .visual-label {
  display: none;
}

.visual.has-poster .visual-inner {
  position: relative;
  align-self: end;
  justify-self: start;
  padding: var(--space-4);
}

.visual.has-poster .visual-desc {
  font-family: var(--font-data);
  font-size: 0.72rem;
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--paper);
}

/* ---- Chips — technology footprints ---------------------------------------- */
.chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  list-style: none;
  padding: 0;
  margin: 0 0 var(--space-4);
}

.chip {
  font-family: var(--font-data);
  font-size: 0.75rem;
  font-weight: 500;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--ink);
  background: var(--paper);
  border: 1px solid var(--ink-2);
  border-radius: var(--radius-sm);
  padding: 0.26rem 0.6rem;
  margin: 0;
  white-space: nowrap;
}

/* ---- Module overview cards (whole card is a link) -------------------------- */
a.module-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  color: inherit;
  text-decoration: none;
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}

a.module-card:hover,
a.module-card:focus-visible {
  text-decoration: none;
  border-color: var(--ink);
  box-shadow: var(--shadow-lift);
}

.card-media {
  display: block;
  margin: calc(-1 * var(--space-6)) calc(-1 * var(--space-6)) var(--space-1);
  aspect-ratio: 16 / 9;
  overflow: hidden;
  border-radius: var(--radius) var(--radius) 0 0;
  border-bottom: 1px solid var(--hairline);
  background: var(--substrate);
}

.card-media img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.module-card h3 {
  color: var(--ink);
  margin-bottom: 0;
}

.module-card p {
  margin-bottom: 0;
}

.module-card .go {
  margin-top: auto;
  font-family: var(--font-data);
  font-size: 0.72rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--accent-dark);
}

.module[id],
section[id] {
  scroll-margin-top: 88px;
}

/* ---- Signal chain — the routed step path ------------------------------------ */
/* The seven-step sequence drawn as one power net: current enters pad 01 and is
   routed pad to pad — orthogonal runs joined by 45° corners, board-fashion —
   until it reaches 07. On wide screens the stations zigzag across the board so
   the *route* carries the order, not the reading direction; narrow screens
   fall back to a single bus down the left. Each connector is two background
   layers on li::after (a vertical run + a 45° corner square), so the path
   needs no extra markup, and it draws in on the same ladder its pad
   energizes on — the current is watchable from 01 through 07. */
.steps {
  --gutter: 88px;
  --row-gap: var(--space-8);
  --pad-size: 44px;
  --pad-y: 22px; /* pad center, from the station's top edge */
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  row-gap: var(--row-gap);
  counter-reset: step;
}

.steps li {
  --i: 0; /* position on the net — drives the arrival ladder */
  position: relative;
  padding-left: calc(var(--pad-size) + var(--space-4));
  counter-increment: step;
  margin: 0;
  color: var(--ink);
}

.steps li:nth-child(2) { --i: 1; }
.steps li:nth-child(3) { --i: 2; }
.steps li:nth-child(4) { --i: 3; }
.steps li:nth-child(5) { --i: 4; }
.steps li:nth-child(6) { --i: 5; }
.steps li:nth-child(7) { --i: 6; }

/* Pad — the numbered via the current arrives at. */
.steps li::before {
  content: counter(step, decimal-leading-zero);
  position: absolute;
  left: 0;
  top: calc(var(--pad-y) - var(--pad-size) / 2);
  width: var(--pad-size);
  height: var(--pad-size);
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1.5px solid var(--accent);
  border-radius: 50%;
  background: var(--paper);
  font-family: var(--font-data);
  font-variant-numeric: tabular-nums;
  font-weight: 500;
  font-size: 1.05rem;
  color: var(--accent-dark);
  z-index: 2;
}

/* Trace — the run out of this pad and into the next one. Narrow screens: a
   straight bus segment. Wide screens (below): the routed 45° connector. */
.steps li::after {
  content: "";
  position: absolute;
  top: var(--pad-y);
  left: calc(var(--pad-size) / 2 - 1px);
  width: 2px;
  height: calc(100% + var(--row-gap));
  background: var(--accent);
  background-repeat: no-repeat;
}

.steps li:last-child::after {
  display: none;
}

/* ---- Stations zigzagged across the board (wide) ---- */
@media (min-width: 880px) {
  .steps {
    --pad-y: 30px;
    grid-template-columns: 1fr 1fr;
    column-gap: var(--gutter);
  }

  /* One station per row, alternating sides, so DOM order and reading order
     stay identical while the route does the sequencing. */
  .steps li:nth-child(1) { grid-area: 1 / 1; }
  .steps li:nth-child(2) { grid-area: 2 / 2; }
  .steps li:nth-child(3) { grid-area: 3 / 1; }
  .steps li:nth-child(4) { grid-area: 4 / 2; }
  .steps li:nth-child(5) { grid-area: 5 / 1; }
  .steps li:nth-child(6) { grid-area: 6 / 2; }
  .steps li:nth-child(7) { grid-area: 7 / 1; }

  /* Each station is a placed component; its pad straddles the inner edge
     where the net lands, so the trace terminates on the part, not beside it. */
  .steps li {
    border: 1px solid var(--hairline);
    border-radius: var(--radius);
    background: var(--paper);
    padding: 1.15rem 1.4rem;
  }

  .steps li:nth-child(odd) {
    padding-right: calc(var(--pad-size) / 2 + var(--space-6));
  }

  .steps li:nth-child(even) {
    padding-left: calc(var(--pad-size) / 2 + var(--space-6));
  }

  .steps li:nth-child(odd)::before {
    left: auto;
    right: calc(var(--pad-size) / -2);
  }

  .steps li:nth-child(even)::before {
    left: calc(var(--pad-size) / -2);
  }

  /* The connector: a vertical run down the gutter, then one 45° corner into
     the opposite pad. Its box spans pad-to-pad exactly (height = station
     height + row gap), so the corner lands on the next via whatever the copy
     does. Layer 1 is the run; layer 2 is the corner square, and being square
     it is a true 45°. */
  .steps li::after {
    width: var(--gutter);
    height: calc(100% + var(--row-gap));
    background-color: transparent;
    background-size: 2px calc(100% - var(--gutter)), var(--gutter)
      var(--gutter);
  }

  .steps li:nth-child(odd)::after {
    left: 100%;
    background-image: linear-gradient(var(--accent), var(--accent)),
      linear-gradient(
        to top right,
        transparent calc(50% - 1px),
        var(--accent) calc(50% - 1px),
        var(--accent) calc(50% + 1px),
        transparent calc(50% + 1px)
      );
    background-position: left top, left bottom;
  }

  .steps li:nth-child(even)::after {
    left: auto;
    right: 100%;
    background-image: linear-gradient(var(--accent), var(--accent)),
      linear-gradient(
        to bottom right,
        transparent calc(50% - 1px),
        var(--accent) calc(50% - 1px),
        var(--accent) calc(50% + 1px),
        transparent calc(50% + 1px)
      );
    background-position: right top, right bottom;
  }
}

/* ---- Current flowing the net ---- */
/* Everything above is the finished, energized board — what no-JS and
   reduced-motion visitors get. reveal.js's .powerup adds the transient: the
   station arrives, its pad lights, and the trace draws downstream into the
   next one, station by station from 01 to 07. The ladder is authored here
   rather than taken from --powerup-delay, which caps at five siblings and
   would land 06 and 07 together. */
.steps li.powerup {
  transition-delay: calc(var(--i) * 220ms);
}

.steps li.powerup::before {
  border-color: var(--hairline);
  color: var(--ink-2);
  transition: border-color 0.22s ease, color 0.22s ease;
  transition-delay: calc(var(--i) * 220ms);
}

.steps li.powerup.is-live::before {
  border-color: var(--accent);
  color: var(--accent-dark);
}

.steps li.powerup::after {
  clip-path: inset(0 0 100% 0);
  transition: clip-path 0.24s linear;
  transition-delay: calc(var(--i) * 220ms + 40ms);
}

.steps li.powerup.is-live::after {
  clip-path: inset(0 0 0 0);
}

/* ---- Continuity checklist ---------------------------------------------------- */
.value-list {
  list-style: none;
  padding: 0;
  margin: 0;
  display: grid;
  gap: var(--space-3);
}

@media (min-width: 720px) {
  .value-list {
    grid-template-columns: 1fr 1fr;
    gap: var(--space-3) var(--space-8);
  }
}

.value-list li {
  position: relative;
  padding-left: var(--space-8);
  margin-bottom: 0;
  color: var(--ink);
}

.value-list li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.38em;
  width: 13px;
  height: 7px;
  border-left: 2px solid var(--accent-dark);
  border-bottom: 2px solid var(--accent-dark);
  transform: rotate(-45deg);
}

/* ---- Probe list (question lists) ---------------------------------------------- */
.q-list {
  list-style: none;
  padding: 0;
  margin: 0 0 var(--space-4);
}

.q-list li {
  position: relative;
  padding-left: var(--space-6);
  color: var(--ink);
}

.q-list li::before {
  content: "\203A";
  position: absolute;
  left: 0;
  top: -0.05em;
  color: var(--accent-dark);
  font-weight: 700;
  font-size: 1.1em;
}

/* ---- Callout — schematic block ------------------------------------------------- */
.callout {
  position: relative;
  background: var(--paper);
  border: 1.5px solid var(--ink);
  border-radius: var(--radius);
  padding: var(--space-8);
}

.callout-kicker {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  font-family: var(--font-data);
  font-size: 0.72rem;
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--accent-dark);
  margin-bottom: var(--space-4);
}

.callout-lead {
  font-family: var(--font-display);
  font-size: clamp(1.2rem, 1.05rem + 0.7vw, 1.5rem);
  font-weight: 650;
  line-height: 1.25;
  color: var(--ink);
  margin-bottom: var(--space-3);
}

.callout-body {
  color: var(--ink-2);
  max-width: 60ch;
  margin-bottom: var(--space-6);
}

.callout-tech {
  padding-top: var(--space-6);
  border-top: 1px solid var(--hairline);
}

.callout-tech-label {
  font-family: var(--font-data);
  font-size: 0.75rem;
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-2);
  margin-bottom: var(--space-3);
}

.callout-tech .chips {
  margin-bottom: var(--space-3);
}

.callout-note {
  font-size: 0.85rem;
  color: var(--ink-2);
  margin-bottom: 0;
}

/* ---- Revision history (About timeline) ------------------------------------------ */
.rev-history {
  list-style: none;
  margin: 0;
  padding: 0;
}

.rev-history li {
  position: relative;
  display: grid;
  grid-template-columns: 96px 1fr;
  gap: var(--space-6);
  padding: var(--space-4) 0 var(--space-4) var(--space-6);
  border-left: 1px solid var(--hairline);
}

.rev-history li::before {
  content: "";
  position: absolute;
  left: -5px;
  top: 1.45em;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--paper);
  border: 2px solid var(--accent-dark);
}

.rev-history .rev-date {
  font-family: var(--font-data);
  font-variant-numeric: tabular-nums;
  font-weight: 500;
  font-size: 0.88rem;
  color: var(--ink);
  padding-top: 0.15em;
}

.rev-history .rev-body {
  color: var(--ink-2);
}

.rev-history .rev-body strong {
  display: block;
  color: var(--ink);
  margin-bottom: 2px;
}

@media (max-width: 560px) {
  .rev-history li {
    grid-template-columns: 1fr;
    gap: var(--space-1);
  }
}

/* ---- Video slot (click-to-load facade) -------------------------------------------- */
.video {
  position: relative;
  aspect-ratio: 16 / 9;
  border-radius: var(--radius);
  background: var(--substrate);
  border: 1px solid var(--hairline);
  display: grid;
  place-items: center;
  overflow: hidden;
  text-align: center;
}

a.video {
  text-decoration: none;
  transition: border-color 0.15s ease;
}

a.video:hover,
a.video:focus-visible {
  text-decoration: none;
  border-color: var(--ink);
}

.video-inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-6);
}

/* Poster — the still that stands in for the player until the visitor presses
   play. Self-hosted, which is what keeps the privacy and cookie sheets true:
   nothing reaches YouTube before that click. */
.video-poster {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* A posted facade moves its control off the artwork rather than covering it.
   The pad and its label sit on the poster's bottom-left corner, the way a
   control is placed on a board — the whole tile is the link, so the pad marks
   the action without having to sit in the middle of the frame. The ink title
   goes: the poster carries the product name and the section's own H2 above it
   already reads "Watch AeroCMS in Action". The link's aria-label is unchanged,
   so the accessible name is exactly what it was. */
.video.has-poster .video-inner {
  position: relative;
  align-self: end;
  justify-self: start;
  flex-direction: row;
  align-items: center;
  gap: var(--space-4);
  text-align: left;
}

.video.has-poster .video-title {
  display: none;
}

.video.has-poster .video-note {
  color: var(--paper);
}

/* The poster does not shrink but the tile does, so below 560px the corner
   control has to give way or it climbs into the artwork. 44px is the site's
   minimum hit target — the pad stops exactly there and the label holds its
   size, since the silk band has a floor of its own. */
@media (max-width: 560px) {
  .video.has-poster .video-inner {
    gap: var(--space-3);
    padding: var(--space-4);
  }
  .video.has-poster .video-play {
    width: 44px;
    height: 44px;
  }
  .video.has-poster .video-play::before {
    border-width: 7px 0 7px 12px;
    margin-left: 3px;
  }
}

.video-play {
  width: 62px;
  height: 62px;
  border-radius: 50%;
  background: var(--signal-red);
  display: grid;
  place-items: center;
  box-shadow: var(--shadow-lift);
  transition: transform 0.2s ease;
}

.video-play::before {
  content: "";
  width: 0;
  height: 0;
  border-style: solid;
  border-width: 10px 0 10px 17px;
  border-color: transparent transparent transparent #ffffff;
  margin-left: 4px;
}

a.video:hover .video-play,
a.video:focus-visible .video-play {
  transform: scale(1.06);
}

.video-title {
  font-family: var(--font-display);
  font-weight: 650;
  color: var(--ink);
  font-size: 1.05rem;
}

.video-note {
  font-family: var(--font-data);
  font-size: 0.72rem;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--ink-2);
}

.video-frame {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9;
  border: 0;
  border-radius: var(--radius);
}

.media-caption {
  margin: var(--space-4) 0 0;
  font-size: 0.95rem;
  color: var(--ink-2);
  max-width: 72ch;
}

/* The click-to-load facade's disclosure. Consent has to be *informed before
   the act*, and the act here is pressing play — so what YouTube will do has
   to be legible at the control, not only in the Cookie Policy. Sits directly
   under the poster, quieter than the caption, so it reads as a footnote to
   the control rather than as content. */
.video-consent {
  margin: var(--space-2) 0 0;
  font-size: 0.78rem;
  line-height: 1.5;
  color: var(--ink-2);
  max-width: 72ch;
}

/* ---- Inspection log (News) ---------------------------------------------------------- */
.press-year {
  font-family: var(--font-data);
  font-size: 0.78rem;
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink);
  margin: var(--space-12) 0 var(--space-3);
  display: flex;
  align-items: center;
  gap: var(--space-3);
}

.press-year::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--hairline);
}

.press-year:first-child {
  margin-top: 0;
}

.press-log {
  list-style: none;
  margin: 0;
  padding: 0;
}

.press-entry {
  display: grid;
  gap: var(--space-2);
  padding: var(--space-6) 0;
  border-bottom: 1px solid var(--hairline);
}

@media (min-width: 760px) {
  .press-entry {
    grid-template-columns: 168px 1fr;
    gap: var(--space-8);
    align-items: start;
  }
}

.press-meta {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.press-date {
  font-family: var(--font-data);
  font-variant-numeric: tabular-nums;
  font-weight: 500;
  font-size: 0.88rem;
  color: var(--ink);
}

.press-outlet {
  font-family: var(--font-data);
  font-size: 0.72rem;
  font-weight: 500;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--ink-2);
}

.press-body {
  min-width: 0;
}

.press-latest {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  font-family: var(--font-data);
  font-size: 0.72rem;
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--signal-red-dark);
  margin-bottom: var(--space-2);
}

/* The newest entry carries the live LED — same pulse as the nav PWR light. */
.press-latest::before {
  content: "";
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--signal-red);
  box-shadow: 0 0 0 3px rgba(var(--signal-red-rgb), 0.15);
  animation: led-pulse 2.6s ease-in-out infinite;
}

.press-headline {
  font-family: var(--font-display);
  font-size: 1.16rem;
  font-weight: 650;
  line-height: 1.32;
  margin: 0 0 var(--space-2);
  color: var(--ink);
}

.press-headline a {
  color: inherit;
}

.press-headline a:hover,
.press-headline a:focus-visible {
  color: var(--accent-dark);
  text-decoration: none;
}

.press-headline a[target="_blank"]::after {
  content: " \2197";
  font-family: var(--font-data);
  font-size: 0.78em;
  font-weight: 500;
  color: var(--accent-dark);
}

.press-summary {
  color: var(--ink-2);
  max-width: 66ch;
  margin: 0;
}

.press-tag {
  display: inline-block;
  font-family: var(--font-data);
  font-size: 0.75rem;
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink);
  border: 1px solid var(--ink-2);
  border-radius: var(--radius-sm);
  padding: 0.18rem 0.5rem;
  margin-top: var(--space-3);
}

.press-also {
  margin: var(--space-3) 0 0;
  font-family: var(--font-data);
  font-size: 0.72rem;
  letter-spacing: 0.03em;
  color: var(--ink-2);
  line-height: 1.9;
}

.press-also-label {
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

.press-also a {
  color: var(--ink-2);
  text-decoration: underline;
  text-decoration-color: var(--hairline);
}

.press-also a:hover,
.press-also a:focus-visible {
  color: var(--accent-dark);
}

/* ---- Release pages (news/*.html) ------------------------------------------------------ */
.release-date {
  font-family: var(--font-data);
  font-variant-numeric: tabular-nums;
  font-size: 0.82rem;
  font-weight: 500;
  letter-spacing: 0.04em;
  color: var(--ink-2);
  margin-top: var(--space-4);
  margin-bottom: 0;
}

/* Section headings on the long reading sheets (legal pages, news releases).
   These sat at 1.1rem against 1rem body — a 1.10 step, which is no step at
   all: the headings that structure a document a vendor reviewer scans were
   the same visual weight as the copy between them. 1.5rem restores a real
   ladder (1.5x over body) while staying well under the 2.2rem section h2 of
   the marketing sheets, which is the right subordination for a prose column. */
.release-body h2 {
  font-size: 1.5rem;
  margin-top: var(--space-10);
  margin-bottom: var(--space-3);
}

.release-back {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  margin-top: var(--space-10);
  font-family: var(--font-data);
  font-size: 0.76rem;
  font-weight: 500;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

/* ---- Legal tables ------------------------------------------------------------------------ */
.table-scroll {
  overflow-x: auto;
  margin: var(--space-6) 0;
  -webkit-overflow-scrolling: touch;
}

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

.legal-table {
  width: 100%;
  min-width: 34rem;
  border-collapse: collapse;
  font-size: 0.94rem;
  color: var(--ink-2);
}

.legal-table th,
.legal-table td {
  text-align: left;
  vertical-align: top;
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--hairline);
}

.legal-table thead th {
  font-family: var(--font-data);
  font-size: 0.72rem;
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink);
  border-bottom: 1.5px solid var(--ink);
}

.legal-table code {
  font-family: var(--font-data);
  font-size: 0.85em;
  color: var(--ink);
}

/* ---- Footer — the bottom silk + title block ------------------------------------------------ */
.site-footer {
  border-top: 2px solid var(--ink);
  background: var(--substrate);
  padding: var(--space-12) 0 var(--space-8);
  margin-top: var(--space-16);
}

.footer-grid {
  display: grid;
  gap: var(--space-8);
  margin-bottom: var(--space-8);
}

@media (min-width: 720px) {
  .footer-grid {
    grid-template-columns: 1.2fr 1fr 1fr;
  }
}

.footer-grid h2 {
  font-family: var(--font-data);
  font-size: 0.74rem;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--ink-2);
  margin-bottom: var(--space-3);
}

.footer-grid ul {
  list-style: none;
  padding: 0;
  margin: 0;
}

/* The row carries the 44px tap floor itself, not just the anchor inside it, so
   a plain-text entry (the street address) stands as tall as the links around
   it — 26px against 44px read as a ragged step in the Contact column. The
   inherited li margin from base.css still sets the gap between rows. */
.footer-grid li {
  display: flex;
  align-items: center;
  min-height: 44px;
}

.footer-grid a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  color: var(--ink);
}

.footer-brand .wordmark {
  line-height: 0;
  margin-bottom: var(--space-4);
}

.footer-brand .wordmark-img {
  height: 1.55rem;
}

/* Title block — the drawing's data table: real identifiers only. */
.title-block {
  border: 1.5px solid var(--ink);
  border-radius: var(--radius);
  background: var(--paper);
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  margin-bottom: var(--space-8);
}

.title-block > div {
  padding: var(--space-3) var(--space-4);
  border-right: 1px solid var(--hairline);
  min-width: 0;
}

.title-block > div:last-child {
  border-right: none;
}

.title-block dt {
  font-family: var(--font-data);
  font-size: 0.72rem;
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-2);
  margin: 0 0 2px;
}

.title-block dd {
  font-family: var(--font-data);
  font-variant-numeric: tabular-nums;
  font-size: 0.84rem;
  font-weight: 500;
  color: var(--ink);
  margin: 0;
  overflow-wrap: anywhere;
}

/* Two columns on a phone, with every rule drawn by grid position — a title
   block whose linework stops halfway across or doubles its own frame reads as
   a bad drawing. An odd last cell spans the full width so the block closes on
   a complete row instead of an orphan half-cell; right-column cells drop the
   divider that would sit flush inside the outer border; and the last row drops
   the bottom rule the outer border already draws. */
@media (max-width: 720px) {
  .title-block {
    grid-template-columns: 1fr 1fr;
  }
  .title-block > div {
    border-bottom: 1px solid var(--hairline);
  }
  .title-block > div:nth-child(even) {
    border-right: none;
  }
  .title-block > div:last-child:nth-child(odd) {
    grid-column: 1 / -1;
  }
  .title-block > div:last-child,
  .title-block > div:nth-last-child(2):nth-child(odd) {
    border-bottom: none;
  }
}

.footer-bottom {
  border-top: 1px solid var(--hairline);
  padding-top: var(--space-6);
  font-family: var(--font-data);
  font-size: 0.76rem;
  color: var(--ink-2);
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
  justify-content: space-between;
}

.footer-legal a {
  position: relative;
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

/* These sit inline in a middot-separated span, so a min-height would do
   nothing to an inline box and an inline-flex swap would break the separator
   rhythm. Extend the tap area past the text instead: 15px of type becomes a
   45px target without moving a pixel of the footer's layout. Horizontal
   inset stays inside the middot gap so the two never overlap. */
.footer-legal a::after {
  content: "";
  position: absolute;
  inset: -15px -5px;
}

.footer-legal a:hover,
.footer-legal a:focus-visible {
  color: var(--accent-dark);
}

/* ---- Power-up reveal ------------------------------------------------------------------------- */
/* One motion rule sitewide: current flows in. assets/js/reveal.js adds
   .powerup (hidden) itself and then .is-live, so with no JS, reduced motion,
   or no IntersectionObserver everything is simply visible. */
.powerup {
  opacity: 0;
  transform: translateY(12px);
  transition: opacity 0.55s cubic-bezier(0.19, 1, 0.22, 1),
    transform 0.55s cubic-bezier(0.19, 1, 0.22, 1);
  transition-delay: var(--powerup-delay, 0ms);
}

.powerup.is-live {
  opacity: 1;
  transform: none;
}

/* ---- Consent bar (analytics opt-in) ------------------------------------------------------------------ */
/* Built and inserted by assets/js/consent.js, never by page markup — without
   JS there is no analytics to consent to, so there is nothing to show. A bar,
   not a wall: the page behind it stays readable and operable, and the two
   choices carry identical weight (same class, same size, same order every
   time), because a Reject that is harder to reach than Accept is not consent. */
.consent-bar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 25; /* over the sticky header (20), under the skip link (30) */
  border-top: 1.5px solid var(--ink);
  background: var(--paper);
  box-shadow: 0 -1px 2px rgba(1, 8, 17, 0.05), 0 -14px 34px rgba(1, 8, 17, 0.09);
  /* Resting state is off-screen: the bar is inserted already hidden and only
     then gets .is-shown, so it rises into place instead of blinking in. */
  opacity: 0;
  visibility: hidden;
  transform: translateY(100%);
  transition: opacity 0.3s cubic-bezier(0.19, 1, 0.22, 1),
    transform 0.3s cubic-bezier(0.19, 1, 0.22, 1), visibility 0s linear 0.3s;
}

.consent-bar.is-shown {
  opacity: 1;
  visibility: visible;
  transform: none;
  transition: opacity 0.3s cubic-bezier(0.19, 1, 0.22, 1),
    transform 0.3s cubic-bezier(0.19, 1, 0.22, 1), visibility 0s linear 0s;
}

.consent-bar:focus {
  outline: none; /* focused only programmatically, when reopened from the footer */
}

.consent-bar-inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4) var(--space-8);
  max-width: var(--container-max);
  margin: 0 auto;
  padding: var(--space-4);
}

.consent-copy {
  flex: 1 1 26rem;
  min-width: 0;
}

/* In-page consent controls (the Cookie Policy carries one) ship with `hidden`
   and are revealed by consent.js, so a build with no measurement id shows no
   dead button. The browser's own [hidden] rule is not enough: it is a
   user-agent style, and *any* author rule beats it — `.btn`'s display:
   inline-flex was quietly winning, leaving the button on screen in exactly
   the default, analytics-off build. This is author-level, and later in the
   file than .btn, so it settles the tie on source order. */
[data-consent-settings][hidden] {
  display: none;
}

/* Same trap, guarded generally for anything inside the bar: `hidden` must
   actually hide, whatever author display rule an element picks up. */
.consent-bar [hidden] {
  display: none;
}

/* Current state, shown only when a choice already exists — reads as a meter
   reading, so it takes the data face like the title above it. */
.consent-copy .consent-state {
  margin-top: 0.3rem;
  font-family: var(--font-data);
  font-size: 0.72rem;
  letter-spacing: 0.04em;
  color: var(--ink);
}

.consent-copy p {
  margin: 0;
  font-size: 0.88rem;
  line-height: 1.55;
  color: var(--ink-2);
}

/* Reads as an instrument label, like every other eyebrow on the board.
   Scoped through .consent-copy to out-specify the rule above rather than
   reaching for !important. */
.consent-copy .consent-title {
  margin-bottom: 0.15rem;
  font-family: var(--font-data);
  font-size: 0.72rem;
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink);
}

.consent-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
}

.consent-actions .btn {
  min-width: 8.5rem;
}

@media (max-width: 640px) {
  .consent-actions {
    width: 100%;
  }

  .consent-actions .btn {
    flex: 1 1 0;
    min-width: 0;
  }
}

/* The return-to-top pad parks in the same bottom strip the bar occupies, at a
   lower layer. Rather than measure a bar whose height depends on how the copy
   wraps, stand the pad down while the bar is up — it is a convenience, the
   consent choice is not, and it comes straight back once the bar closes. The
   :has() selector out-specifies .to-top-js .to-top.is-shown, so it needs no
   !important; visibility is what keeps the stood-down pad out of the tab order. */
body:has(.consent-bar.is-shown) .to-top {
  opacity: 0;
  visibility: hidden;
  transform: translateY(8px);
}
