/* ==========================================================================
   K9 Interdiction and Detection Services — styles.css
   Clean & institutional design system. No external fonts, no framework.
   Accent hue is teal/steel by explicit requirement - no gold, orange,
   yellow, pink, brown, or variants anywhere in this file.
   ========================================================================== */

/* ---- Reset ---- */
*, *::before, *::after { box-sizing: border-box; }
html, body, h1, h2, h3, h4, p, figure, blockquote, dl, dd, ul, ol { margin: 0; }
ul[class], ol[class] { padding: 0; list-style: none; }
img, svg { display: block; max-width: 100%; }
input, button, textarea, select { font: inherit; color: inherit; }
button { cursor: pointer; }
/* Links are never underlined anywhere on this site, and never change colour
   on click or once visited - only on hover.
   NOTE: do NOT add `a:visited`/`a:active` rules here. Those selectors are
   (0,1,1) and would outrank every single-class component rule like
   .btn--primary (0,1,0), so a visited Contact button rendered dark-on-dark.
   `a { color: inherit }` alone is sufficient: author styles beat the UA
   stylesheet's visited colour regardless of specificity. */
a { color: inherit; text-decoration: none; }

/* ---- Design tokens ---- */
:root {
  --color-bg: #F6F7F8;
  --color-surface: #FFFFFF;
  --color-ink: #1A1D21;
  --color-ink-muted: #5B6672;
  --color-navy: #14283D;
  --color-navy-dark: #0A1826;
  --color-navy-light: #1C3552;
  --color-accent: #2A98A5;
  --color-accent-dark: #1B717C;
  --color-accent-deep: #12626D;
  --color-accent-light: #7FCBD3;
  --color-accent-lighter: #A9DDE3;
  --color-accent-rgb: 42,152,165;
  --color-border: #E3E6E9;
  /* Separate, darker token for INTERACTIVE boundaries. The hairline
     --color-border is only ~1.25:1 on white, which fails WCAG 1.4.11 for
     form controls; this clears the 3:1 non-text contrast requirement. */
  --color-border-input: #8A929B;
  --color-success: #2F6B4F;
  --color-success-bg: #EAF3EE;
  --color-error: #B4342E;

  --gradient-navy: linear-gradient(135deg, var(--color-navy-light) 0%, var(--color-navy-dark) 100%);
  --gradient-accent: linear-gradient(135deg, var(--color-accent-lighter) 0%, var(--color-accent) 100%);
  --gradient-accent-hover: linear-gradient(135deg, var(--color-accent-light) 0%, var(--color-accent-dark) 100%);

  /* HEADINGS ONLY. Do NOT set anything containing a digit in this stack.
     Georgia is an old-style-figure font: its 3 4 5 7 9 hang below the
     baseline and its 0 1 2 sit at x-height. Measured by rendering the shipped
     /System/.../Georgia.ttf at 160px: the "9" in "K9IDS" drops 28px below the
     baseline (17.5% of the em) while "KIDS" alone drops 2px - so the company's
     own wordmark read as "KgIDS", twice on every page, at the largest size any
     visitor sees it.
     font-variant-numeric: lining-nums CANNOT fix this: the same file has no
     `lnum` feature at all (checked for the raw OpenType tag in the binary - it
     is absent from Georgia, Georgia Bold and Times New Roman alike). Lining
     figures would need a different face, and self-hosting one is the correct
     eventual fix; until a licensed woff2 pair is actually in assets/fonts/,
     every numeral-bearing element uses --font-body, whose system faces
     (SF/Segoe UI/Roboto/Helvetica/Arial) all have lining figures. See
     .brand__text and .pillar__num. */
  --font-heading: Georgia, 'Times New Roman', serif;
  --font-body: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;

  --radius: 6px;
  --radius-lg: 12px;
  --shadow-sm: 0 1px 3px rgba(10,24,38,0.08);
  --shadow-md: 0 10px 28px rgba(10,24,38,0.12);
  --shadow-lg: 0 18px 44px rgba(10,24,38,0.20);
  --shadow-header: 0 1px 0 rgba(10,24,38,0.06), 0 8px 24px rgba(10,24,38,0.05);
  --shadow-header-scrolled: 0 4px 24px rgba(10,24,38,0.14);
  --container-width: 1280px;

  --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
  --transition-fast: 0.18s var(--ease-out);
  --transition-med: 0.32s var(--ease-out);

  /* Type scale. Replaces ~23 ad-hoc rem values, four of which differed by a
     single pixel (0.95/0.96/0.97/0.98rem) and so were not distinct sizes at
     all - just noise that made the page look unconsidered. */
  --fs-xs: 0.78rem;   /* eyebrows, meta, legal fine print */
  --fs-sm: 0.88rem;   /* captions, secondary UI, footer links */
  --fs-md: 0.97rem;   /* body copy inside cards */
  --fs-base: 1.05rem; /* long-form body copy */
  --fs-lg: 1.15rem;   /* lede */
  --fs-xl: 1.35rem;   /* h3 */
  --fs-2xl: 1.6rem;   /* sub-section headings */

  /* Spacing scale, replacing ~30 one-off rem values. */
  --sp-1: 0.35rem;
  --sp-2: 0.6rem;
  --sp-3: 0.9rem;
  --sp-4: 1.25rem;
  --sp-5: 1.75rem;
  --sp-6: 2.5rem;
  --sp-7: 3.5rem;
}

/* scroll-padding-top is required alongside the sticky header: without it an
   in-page anchor (e.g. /services#firearms) scrolls the target flush to the
   viewport top and the sticky header covers the heading. */
html {
  scroll-behavior: smooth;
  scroll-padding-top: 6rem;
}

/* Sticky footer: without this the footer floats mid-viewport on short
   pages (privacy/terms/sitemap/404) leaving a bare strip beneath it. */
body {
  background: var(--color-bg);
  color: var(--color-ink);
  font-family: var(--font-body);
  font-size: 16px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}
main { flex: 1 0 auto; }
.site-footer { flex-shrink: 0; }

h1, h2, h3, h4 {
  font-family: var(--font-heading);
  color: var(--color-navy);
  line-height: 1.2;
  font-weight: 600;
  /* Only affects how lines are distributed when a heading is genuinely too
     long for its container - it never introduces a wrap that wouldn't
     otherwise happen, so a long title breaks evenly instead of dropping a
     single orphan word onto the last line. */
  text-wrap: balance;
}
h1 { font-size: clamp(2rem, 4vw + 1rem, 3.25rem); font-weight: 700; letter-spacing: -0.015em; }
h2 { font-size: clamp(1.5rem, 2vw + 1rem, 2.25rem); letter-spacing: -0.01em; }
/* h3/h4 were fixed while h1/h2 were fluid, so the gap between h2 and h3
   collapsed on small screens and gaped on large ones. Both now scale. */
h3 { font-size: clamp(1.15rem, 0.5vw + 1rem, var(--fs-xl)); }
h4 { font-size: clamp(1rem, 0.3vw + 0.9rem, 1.1rem); }

p { color: var(--color-ink); }
p + p { margin-top: 1em; }

/* The reset zeroes all heading and paragraph margins, and nothing restored
   the gap BETWEEN a heading and the copy under it - so every h2/h3 sat
   flush against its own body text. This is the single most visible spacing
   defect on the content pages. */
h2 + p, h3 + p, h4 + p,
h2 + ul, h3 + ul, h4 + ul { margin-top: 0.7rem; }
p + h3, p + h4, ul + h3, ul + h4 { margin-top: 1.9rem; }

/* `ch` resolves against this element's OWN computed font-size (the advance
   width of "0" in its font), so a per-element `max-width: NNch` IS the
   correct measure cap. Do not put one on a container - a cap on .wrap would
   resolve against .wrap's font rather than the text inside it. */
.lede {
  font-size: var(--fs-lg);
  color: var(--color-ink-muted);
}

.eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 0.6em;
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--color-accent-dark);
  margin-bottom: 0.7em;
}
.eyebrow::before {
  content: "";
  width: 24px;
  height: 2px;
  background: var(--gradient-accent);
  display: inline-block;
  border-radius: 2px;
}

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  background: var(--color-navy);
  color: #fff;
  padding: 0.75em 1.25em;
  z-index: 1000;
}
.skip-link:focus { left: 1rem; top: 1rem; }

/* Visible keyboard focus everywhere, without showing rings on mouse click. */
a:focus-visible, button:focus-visible, input:focus-visible,
select:focus-visible, textarea:focus-visible, summary:focus-visible {
  outline: 3px solid var(--color-accent);
  outline-offset: 2px;
  border-radius: 3px;
}

/* ---- Layout ---- */
.wrap {
  max-width: var(--container-width);
  margin-inline: auto;
  padding-inline: 1.5rem;
}
section { padding-block: clamp(3.5rem, 6vw, 6rem); position: relative; }
.section--tight { padding-block: clamp(2.5rem, 4vw, 3.5rem); }
.section--surface {
  background: var(--color-surface);
  border-block: 1px solid var(--color-border);
}
.section--navy {
  background: var(--gradient-navy);
  color: #fff;
  position: relative;
  overflow: hidden;
}
.section--navy::before {
  content: "";
  position: absolute;
  inset: 0;
  background: radial-gradient(900px 420px at 90% 0%, rgba(var(--color-accent-rgb),0.18), transparent 60%);
  pointer-events: none;
}
/* Explicit color on every text element nested in a navy section - relying
   on inheritance from .section--navy's own `color` does NOT work here,
   because the blanket `p`/`h1-h4` rules above target the element directly
   and win over inheritance regardless of the parent's specificity. h1 is
   included deliberately: omitting it rendered the 404 headline navy-on-
   navy at 1.2:1 contrast, i.e. invisible. */
.section--navy h1, .section--navy h2, .section--navy h3, .section--navy h4 { color: #fff; }
.section--navy p { color: rgba(255,255,255,0.78); }
.section--navy .lede { color: rgba(255,255,255,0.85); }
.section--navy .eyebrow { color: var(--color-accent-light); }

/* CRITICAL: the rules above target DESCENDANTS, so any light-surfaced
   component nested inside a navy section inherited white text on its white
   background. Every "How a Sweep Works" step title on /services was white
   on white and therefore invisible. Light surfaces must restate their own
   text colours. */
.section--navy .card,
.section--navy .step,
.section--navy .faq-item { color: var(--color-ink); }
.section--navy .card h1, .section--navy .card h2,
.section--navy .card h3, .section--navy .card h4,
.section--navy .step h1, .section--navy .step h2,
.section--navy .step h3, .section--navy .step h4,
.section--navy .faq-item summary { color: var(--color-navy); }
.section--navy .card p, .section--navy .step p,
.section--navy .faq-item p { color: var(--color-ink-muted); }
.section--navy .card .eyebrow, .section--navy .step .eyebrow { color: var(--color-accent-dark); }
.hero .eyebrow { color: var(--color-accent-light); }
.hero__actions--center { justify-content: center; }

.section-head { margin-bottom: 3rem; }
/* Centred heads had no measure cap at all, so ledes ran the full 1232px
   container at roughly 110 characters per line. `.mx-auto` only sets
   margin-inline and supplies no width, so it was a no-op here. */
.section-head--center { margin-inline: auto; text-align: center; max-width: 54rem; }
.section-head--center .lede, .lede.mx-auto { max-width: 42rem; margin-inline: auto; }
/* .page-banner supplies its own bottom spacing; this margin added ~48px of
   dead navy beneath every banner lede. */
.page-banner .section-head { margin-bottom: 0; }
.section-head--center .eyebrow { justify-content: center; }
.section-head .lede { margin-top: 0.9rem; }

.grid { display: grid; gap: 1.75rem; }
/* Explicit tracks. auto-fit emitted a phantom fourth 0px track and orphaned
   the third card 2+1 between roughly 636px and 944px. When it does drop to
   two columns, the odd last child spans the full row so it is never
   visually demoted to half width. */
.grid--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
@media (max-width: 980px) {
  .grid--3 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .grid--3 > :last-child:nth-child(odd) { grid-column: 1 / -1; }
}
@media (max-width: 700px) { .grid--3 { grid-template-columns: 1fr; } }
/* auto-fit is correct for the 2-child text+card pairs (empty tracks collapse),
   but with FOUR children it fits three 392px columns and orphans the fourth.
   Use .grid--2up wherever the count is not two. */
.grid--2 { grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); align-items: stretch; }
/* The 320px floor held two columns down to about a 716px viewport, so an iPad
   Air or 11in iPad Pro in portrait (768-860px) got two 339px columns: roughly
   42 characters of prose and every checklist item wrapping to two lines.
   860px, matching the `.is-reversed` order reset below, so the collapse to one
   column and the un-swap happen at the same width. Deliberately `.grid--2`,
   not `.grid--2 > *` - a child selector here is the trap documented at the
   is-reversed reset, and it would not override the base rule at all. */
@media (max-width: 860px) {
  .grid--2 { grid-template-columns: 1fr; }
}
.grid--2up { grid-template-columns: repeat(2, minmax(0, 1fr)); }
/* `.pillar:first-child` exempts only item 1 from the separating hairline, which
   is right for a vertical list but wrong in a 2x2 grid: item 2 is also on the
   first row, so it drew a rule above itself and the row rendered lopsided.
   (0,3,0) beats `.pillar:first-child` (0,2,0). The base .pillar rules are left
   alone - /services and the home page use them as a genuine vertical list. */
.grid--2up > .pillar:nth-child(-n+2) { border-top: none; }
@media (max-width: 760px) {
  .grid--2up { grid-template-columns: 1fr; }
  /* Single column again, so item 2 is now genuinely below item 1 and needs
     its separator back. Same (0,3,0), later in source, so it wins. */
  .grid--2up > .pillar:nth-child(2) { border-top: 1px solid rgba(255,255,255,0.13); }
}
.grid--2-center { align-items: center; }
/* auto-fit with a 190px floor drops to 4-up between roughly 900px and
   1110px, orphaning the fifth tile on its own row. Explicit breakpoints
   keep the row either 5-up, a balanced 3+2, or 2-up. */
.grid--5 { grid-template-columns: repeat(5, minmax(0, 1fr)); }
@media (max-width: 1180px) { .grid--5 { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (max-width: 720px) { .grid--5 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 420px) { .grid--5 { grid-template-columns: 1fr; } }

/* Mirrored two-column sections. The DOM stays in reading order (text first,
   supporting card second) and only the visual columns swap, so screen
   readers and mobile always get the text first. The previous approach put
   `order` on both children in reversed source order, which cancelled out -
   every section rendered text-left and the page never actually alternated. */
.grid--2.is-reversed > *:first-child { order: 2; }
.grid--2.is-reversed > *:last-child { order: 1; }
@media (max-width: 860px) {
  /* Must repeat :first-child/:last-child. The two rules above are (0,3,0);
     `.grid--2.is-reversed > *` is only (0,2,0), and a media query adds no
     specificity - so the reset silently lost and the order swap stayed on
     down to the narrowest phone. In one column that put each supporting card
     ABOVE the <h2> it supports: on /services the "Commonly Used For" list
     appeared before "Explosives Detection", reading as an answer with no
     question. Measured at 390px: card top 1657, heading top 2083. */
  .grid--2.is-reversed > *:first-child,
  .grid--2.is-reversed > *:last-child { order: 0; }
}

/* ---- Utilities ---- */
.text-center { text-align: center; }
.mx-auto { margin-inline: auto; }
.mt-sm { margin-top: 0.75rem; }
.mt-md { margin-top: 1.25rem; }
.mt-lg { margin-top: 1.75rem; }
.mt-xl { margin-top: 2.25rem; }
.note-inline { font-style: italic; }
.max-w-narrow { max-width: 820px; }
.checklist { display: grid; gap: 0.85rem; margin-top: 1.25rem; }
.checklist li {
  position: relative;
  padding-left: 1.75rem;
  color: var(--color-ink-muted);
  font-size: var(--fs-md);
}
/* Real markers come from CSS, not literal &bull; characters typed into the
   markup - a screen reader announces those as the word "bullet". */
.checklist li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.55em;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--gradient-accent);
}

/* .max-w-narrow caps the CONTAINER at 820px but nothing capped the text
   inside it, so /about's "Straight Talk" section still ran 104 characters per
   line. Capped per element, for the same `ch` reason noted above.
   The commitments list is also promoted out of caption grey: these are the
   page's actual promises, and --color-ink-muted at --fs-md rendered them as
   incidental captions beside the copy that introduces them. All of these beat
   the .checklist rules above on specificity, not on source order. */
.max-w-narrow p, .max-w-narrow .checklist li { max-width: 64ch; }
.max-w-narrow .checklist li { font-size: var(--fs-base); color: var(--color-ink); }
/* The marker is positioned from the top of the item, so a larger font-size
   moves the first line's centre down and the dot has to follow it. */
.max-w-narrow .checklist li::before { top: 0.6em; }

/* ---- Scroll reveal ----
   Elements start hidden ONLY when JS is running (the .js-reveal class is
   added by main.js). Without JS nothing is hidden, so content is never
   trapped behind a script that failed to load. */
.js-reveal { opacity: 0; transform: translateY(12px); }
/* box-shadow and border-color are in this list because they have to be. This
   rule is (0,2,0) and `.is-visible` is never removed, so its `transition`
   SHORTHAND permanently replaces the one on every revealed .card (0,1,0),
   .step, .pillar and .serve-grid tile - and those components animate their
   hover shadow and border through that shorthand. Without them here the hover
   response snapped instantly on every card the observer had revealed, i.e. all
   of them. A shorthand cannot be partially overridden; it has to name
   everything the elements underneath it need. */
.js-reveal.is-visible {
  opacity: 1;
  transform: none;
  transition: opacity 0.35s var(--ease-out), transform 0.35s var(--ease-out),
              box-shadow var(--transition-med), border-color var(--transition-med);
}

/* ---- Buttons ---- */
/* ---- Button system ----
   Every variant shares one height, one radius, a gradient fill, a subtle
   border and a subtle shadow, and every one changes its background on hover.
   Text/background pairs are all chosen to clear WCAG AA in BOTH the resting
   and hover states - the hover state is where contrast is most often lost,
   because that is the moment nobody checks. */
.btn {
  --btn-h: 48px;
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5em;
  min-height: var(--btn-h);
  padding: 0 1.7rem;
  border-radius: var(--radius);
  font-weight: 600;
  font-size: var(--fs-md);
  line-height: 1.2;
  text-align: center;
  border: 1px solid transparent;
  box-shadow: var(--shadow-sm);
  cursor: pointer;
  transition: color var(--transition-fast), background-image var(--transition-fast),
              border-color var(--transition-fast), transform var(--transition-fast),
              box-shadow var(--transition-fast);
}
.btn:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); }
.btn:active { transform: translateY(0); box-shadow: var(--shadow-sm); }
.btn[disabled] { opacity: 0.6; cursor: not-allowed; transform: none; }

/* Navy fill, white text. 13.6:1 resting, 11.4:1 hover. */
.btn--primary {
  background-image: linear-gradient(135deg, #22405F 0%, #0A1826 100%);
  color: #fff;
  border-color: #0A1826;
}
.btn--primary:hover {
  background-image: linear-gradient(135deg, #2C4E71 0%, #12263A 100%);
  color: #fff;
}

/* ONE COLOUR, ONE RANK. This is a rule about meaning, not about styling, and
   it is the only thing keeping the button system readable:

     .btn--accent          the single primary action of its section. One per
                           section. Never two.
     .btn--primary         a secondary action on a light background.
     .btn--outline         a secondary action on a light background, quieter.
     .btn--outline-light   a secondary action on a dark background.

   It drifted once already: "Request a Consultation" was .btn--accent in the
   home hero and .btn--primary in the closing CTA of the same page, where the
   teal had been given to the phone number instead - so the loudest button on
   the page changed meaning between the top and the bottom of one scroll. That
   does not read as a choice; it reads as nobody being in charge of the design.
   If a section seems to need two accent buttons, one of them is not primary.

   Light teal fill, navy text. Navy on the lightest stop is ~11:1 and on the
   darkest ~4.9:1; hover lightens, so contrast only improves. */
.btn--accent {
  background-image: linear-gradient(135deg, #A9DDE3 0%, #38A6B3 100%);
  color: var(--color-navy-dark);
  border-color: #2A98A5;
}
.btn--accent:hover {
  background-image: linear-gradient(135deg, #C6EAEF 0%, #62BEC9 100%);
  color: var(--color-navy-dark);
  border-color: #38A6B3;
}

/* For light backgrounds: near-white gradient, navy text (~13:1). */
.btn--outline {
  background-image: linear-gradient(135deg, #FFFFFF 0%, #EDF1F4 100%);
  color: var(--color-navy);
  border-color: #C3CCD4;
}
.btn--outline:hover {
  background-image: linear-gradient(135deg, #F4F7F9 0%, #DDE4EA 100%);
  color: var(--color-navy-dark);
  border-color: var(--color-navy);
}

/* For dark backgrounds: translucent white gradient over navy, white text.
   Both stops stay dark enough that white text holds well above 7:1. */
.btn--outline-light {
  background-image: linear-gradient(135deg, rgba(255,255,255,0.20) 0%, rgba(255,255,255,0.07) 100%);
  color: #fff;
  border-color: rgba(255,255,255,0.55);
}
.btn--outline-light:hover {
  background-image: linear-gradient(135deg, rgba(255,255,255,0.32) 0%, rgba(255,255,255,0.16) 100%);
  color: #fff;
  border-color: #fff;
}

/* Narrower, not shorter - height stays uniform across every button. */
.btn--small { padding: 0 1.25rem; font-size: var(--fs-sm); }

/* ---- Utility bar ----
   The thin dark strip above the masthead. Two-tier headers are the structural
   convention that reads "organization" rather than "one person with a phone",
   and every fact on this one is already confirmed, so it buys that reading at
   no credibility cost. It also demotes the phone number out of the nav row,
   where it was ranked equal to "About".

   NOT sticky, and deliberately outside <header>: .site-header keeps
   position:sticky to itself, so this strip scrolls away and the sticky
   element's offset stays 0. Do not move it inside the header without
   re-checking html{scroll-padding-top} - every in-page anchor depends on it.

   White at 0.72 alpha over --color-navy-dark measures 9.5:1, so the 12.5px
   type clears WCAG AA with a wide margin; the phone is full white (17.7:1)
   because it is the one thing here anyone acts on. */
.utility-bar {
  background: var(--color-navy-dark);
  color: rgba(255,255,255,0.72);
  font-size: var(--fs-xs);
  line-height: 1.4;
}
.utility-bar__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem 1.5rem;
  padding-block: 0.5rem;
}
/* nowrap on every item. This strip is one row or it is nothing: the moment a
   sentence wraps it doubles the bar's height and the masthead below it starts
   the page 34px lower than it was designed to. The two prose items step out at
   the widths below rather than being allowed to wrap. */
.utility-bar__identity,
.utility-bar__availability,
.utility-bar__phone { white-space: nowrap; }
.utility-bar__identity {
  color: rgba(255,255,255,0.72);
  transition: color var(--transition-fast);
}
.utility-bar__identity:hover { color: var(--color-accent-light); }
.utility-bar__actions { display: flex; align-items: center; gap: 1.25rem; }
.utility-bar__phone {
  display: inline-flex;
  align-items: center;
  gap: 0.45em;
  font-weight: 700;
  color: #fff;
  transition: color var(--transition-fast);
}
.utility-bar__phone svg { flex-shrink: 0; color: var(--color-accent-light); }
.utility-bar__phone:hover { color: var(--color-accent-light); }

/* Declared AFTER the base rules, not merged into the media blocks at the foot
   of this file: both are (0,1,0) and a media query adds no specificity, so a
   hide rule written earlier would silently lose to the base rule. That exact
   mistake is documented three times elsewhere in this file.
   Measured in the browser, not estimated: identity 405px, availability 239px,
   phone 121px, plus 44px of gaps and the container's 48px of padding = 857px
   for all three, and 598px for identity + phone. The thresholds sit well above
   both because these numbers are macOS/SF metrics and Segoe UI on Windows sets
   the same strings several per cent wider - and the failure mode here is not
   subtle, it is the bar wrapping to two lines and pushing the masthead down on
   every page.
   The phone NEVER drops - below 960px .site-nav is display:none, so if this
   strip hid itself on mobile the only tel: link on the page would be locked
   inside the hamburger, which is where it started. */
@media (max-width: 1080px) {
  .utility-bar__availability { display: none; }
}
@media (max-width: 860px) {
  .utility-bar__identity { display: none; }
}

/* ---- Header / Nav ---- */
.site-header {
  position: sticky;
  top: 0;
  z-index: 100;
  background: var(--color-surface);
  box-shadow: var(--shadow-header);
  transition: box-shadow var(--transition-med);
}
.site-header.is-scrolled { box-shadow: var(--shadow-header-scrolled); }
.site-header__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding-block: 0.9rem;
}
.brand { display: inline-flex; align-items: center; gap: 0.75rem; text-decoration: none; }
.brand__lockup { display: flex; flex-direction: column; line-height: 1.1; }
.brand__full {
  font-size: 0.7rem;
  letter-spacing: 0.03em;
  color: var(--color-ink-muted);
  font-weight: 500;
  margin-top: 0.15rem;
  white-space: nowrap;
}
.site-footer .brand__full { color: rgba(255,255,255,0.62); }
/* Below the desktop nav breakpoint the header is tight; the expansion also
   appears in the footer and the hero, so dropping it here loses nothing. */
@media (max-width: 620px) { .brand__full { display: none; } }
/* The mark artwork has an opaque white background. On the white header that
   is invisible; on the navy footer it is set in a rounded white chip so it
   reads as a deliberate badge rather than a floating white square. */
.brand__mark {
  flex-shrink: 0;
  display: block;
  transition: transform var(--transition-med);
}
.brand__mark--dark {
  background: #fff;
  border-radius: 9px;
  padding: 3px;
  box-sizing: content-box;
}
.brand:hover .brand__mark { transform: scale(1.05); }
/* --font-body, NOT --font-heading. This is the one string on the site that
   must never be read wrong, and in Georgia it was: the 9 sat at x-height with
   a descender, so the mark rendered as "KgIDS" (see the measurement on
   --font-heading above). The system sans stack has lining figures everywhere,
   so the 9 now shares a baseline and a cap height with the K.
   Cap heights are within 4% (Georgia Bold 0.695 em, Helvetica Bold 0.720 em),
   so the lockup does not change height; the sans advance is 13% narrower over
   five characters, and the tracking below spends about a third of that back.
   A logotype in a different face from the body headings is normal - that is
   what makes it read as a mark rather than as a heading that happens to be
   first. */
.brand__text {
  font-family: var(--font-body);
  font-weight: 700;
  font-size: var(--fs-xl);
  color: var(--color-navy);
  letter-spacing: 0.03em;
}
/* "IDS" is the meaningful part of the wordmark and has to read as such.
   It was previously a background-clip:text gradient starting at #A9DDE3,
   which is only about 1.7:1 against the white header - the top-left of every
   glyph washed out and the acronym looked faint. Solid deep teal instead:
   5.7:1 on white, still clearly distinct from the navy "K9". */
.brand__accent {
  background-image: none;
  -webkit-background-clip: initial;
  background-clip: initial;
  color: var(--color-accent-deep);
  -webkit-text-fill-color: currentColor;
  /* Tracking is inherited from .brand__text on purpose. This span used to set
     its own 0.005em against the parent's 0.01em, which was invisible in
     Georgia; with the parent now at 0.03em (see .brand__text) the same override
     tracked "K9" at 0.648px and "IDS" at 0.108px - measured - so the wordmark
     had two different letter spacings inside five characters. Colour is the
     only thing this rule should be doing. */
}

.nav-toggle {
  display: none;
  flex-direction: column;
  justify-content: center;
  gap: 5px;
  width: 40px;
  height: 40px;
  background: transparent;
  border: none;
  padding: 0;
}
.nav-toggle span {
  display: block;
  height: 2px;
  width: 100%;
  background: var(--color-navy);
  transition: transform var(--transition-fast), opacity var(--transition-fast);
}
.nav-toggle[aria-expanded="true"] span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
.nav-toggle[aria-expanded="true"] span:nth-child(2) { opacity: 0; }
.nav-toggle[aria-expanded="true"] span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }

.site-nav { display: flex; align-items: center; gap: 1.9rem; }
/* :not(.btn) is load-bearing. `.site-nav a` (0,1,1) outranks `.btn--primary`
   (0,1,0), so without it the nav's ink colour wins and any button in the nav
   renders dark-on-dark. Scope nav link styling to actual links only. */
.site-nav > a:not(.btn):not(.site-nav__phone) {
  position: relative;
  text-decoration: none;
  font-size: var(--fs-md);
  font-weight: 500;
  color: var(--color-ink);
  padding-block: 0.3rem;
  transition: color var(--transition-fast);
}
.site-nav > a:not(.btn):not(.site-nav__phone)::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: 0;
  height: 2px;
  width: 0;
  border-radius: 2px;
  background: var(--gradient-accent);
  transition: width var(--transition-med);
}
.site-nav > a:not(.btn):not(.site-nav__phone):hover { color: var(--color-navy); }
.site-nav > a:not(.btn):not(.site-nav__phone):hover::after,
.site-nav a.is-active:not(.btn)::after { width: 100%; }
.site-nav a.is-active:not(.btn) { color: var(--color-navy); }
.site-nav a.btn { text-decoration: none; }
/* /contact was the only page with no visible current-page indicator: the two
   rules above are the site's only ones and both exclude .btn, and header.php
   marks the Contact button with aria-current but not .is-active. Keyed on the
   attribute that is already in the markup rather than adding a class that
   would then have to fight `.btn--primary` for the underline treatment.
   (0,3,1) clears every .btn rule. The resting shadow is repeated in the
   shorthand because a shorthand replaces, and dropping it would take the
   button's subtle shadow with it; the :hover twin keeps the lift. */
.site-nav a.btn[aria-current="page"] {
  box-shadow: inset 0 0 0 2px var(--color-accent-light), var(--shadow-sm);
}
.site-nav a.btn[aria-current="page"]:hover {
  box-shadow: inset 0 0 0 2px var(--color-accent-light), var(--shadow-md);
}

/* The `.site-nav__phone` rules that used to sit here are gone with the element
   - the number lives in .utility-bar now. The `:not(.site-nav__phone)` in the
   four selectors above is deliberately LEFT IN PLACE. Each `:not()` carries
   the specificity of its argument, so removing it would drop those selectors
   from (0,3,1) to (0,2,1) and re-open the tie with
   `.site-nav a.is-active:not(.btn)::after` that the comments above spent three
   paragraphs settling. It costs nothing and matches nothing. */

/* No border-bottom here - it read as an underline, which this site does not
   use. Weight and colour carry the affordance instead. */
.lede__phone, .contact-aside__phone {
  color: var(--color-accent-deep);
  font-weight: 700;
  transition: color var(--transition-fast);
  white-space: nowrap;
}
.lede__phone:hover, .contact-aside__phone:hover { color: var(--color-navy); }

/* On a dark banner both of those are near-invisible: the deep teal barely
   separates from navy at rest, and the navy hover disappears entirely.
   Light teal at rest, white on hover. */
.page-banner .lede__phone,
.section--navy .lede__phone,
.hero .lede__phone {
  color: var(--color-accent-light);
}
.page-banner .lede__phone:hover,
.section--navy .lede__phone:hover,
.hero .lede__phone:hover {
  color: #fff;
}

.site-footer__phone {
  display: inline-flex;
  align-items: center;
  gap: 0.5em;
  margin-top: 1rem;
  font-size: var(--fs-lg);
  font-weight: 700;
  color: #fff;
  text-decoration: none;
  transition: color var(--transition-fast);
}
.site-footer__phone svg { color: var(--color-accent-light); flex-shrink: 0; }
.site-footer__phone:hover { color: var(--color-accent-light); }

/* ---- Hero ---- */
/* Top padding only: the hero closes with .hero__bar, which must sit flush on
   the section's bottom edge rather than floating above a padding gap.
   The navy gradient is the BASE layer, so if the photograph is missing or
   still downloading the hero is never broken - just un-photographed. */
.hero {
  padding-top: clamp(3.5rem, 7vw, 6rem);
  padding-bottom: 0;
  background:
    radial-gradient(1100px 480px at 85% -10%, rgba(var(--color-accent-rgb),0.20), transparent 60%),
    var(--gradient-navy);
  color: #fff;
  position: relative;
  overflow: hidden;
}
/* Photograph layer. Sits above the gradient, below the scrim and content.
   Swap the file at /assets/img/hero-detection-team.jpg to change it - and
   regenerate its .webp sibling in the same pass (cwebp -q 78), or the WebP
   below will keep serving the OLD picture to almost every visitor while the
   JPEG you just replaced is only ever seen by the fallback.

   TWO background-image declarations, in this order, on purpose. The plain
   url() is the floor: any engine that does not parse image-set() or its
   type() argument discards the second declaration and keeps the first, so the
   masthead can never end up with no photograph. The second is what modern
   browsers actually use - measured, this hero is 215,510 bytes as JPEG and
   58,430 as WebP (-73%), and it is the LCP element on the home page.
   The preload in includes/header.php names the .webp and gates it on
   type="image/webp"; the two must stay in step or the browser fetches both.

   One known, accepted gap: Safari 16 and earlier support WebP and support
   preload's type gate, but got image-set()'s type() only in Safari 17. Those
   versions therefore preload the WebP and then take the JPEG from this rule -
   one wasted fetch. It cannot be fixed with -webkit-image-set(), which carries
   resolution but has no type(), and it is not worth pushing the whole site
   back to JPEG-only for. */
.hero::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image: url('/assets/img/hero-detection-team.jpg');
  background-image: image-set(url('/assets/img/hero-detection-team.webp') type('image/webp'),
                              url('/assets/img/hero-detection-team.jpg') type('image/jpeg'));
  background-size: cover;
  background-position: 72% center;
  opacity: 0.55;
  z-index: 0;
  pointer-events: none;
}
/* Scrim: keeps the headline legible over whatever the photo turns out to be.
   Dense navy on the left where the text sits, clearing toward the right so
   the image is actually visible. */
.hero::after {
  content: "";
  position: absolute;
  inset: 0;
  background:
    linear-gradient(90deg, var(--color-navy-dark) 0%, rgba(10,24,38,0.94) 34%, rgba(10,24,38,0.62) 62%, rgba(10,24,38,0.34) 100%),
    linear-gradient(180deg, rgba(10,24,38,0.55) 0%, transparent 28%, rgba(10,24,38,0.45) 100%);
  z-index: 1;
  pointer-events: none;
}
@media (max-width: 860px) {
  /* On narrow screens the text spans the full width, so the scrim has to be
     even across it rather than clearing to the right.
     It was TOO even: 0.55 image opacity under an 0.80-0.86 scrim left about
     11% of the photograph showing, so a phone visitor got a plain navy panel
     with a faint smudge - no dog, no handler, no context - on the page whose
     job is to sell detection dog teams, while still paying 60 KB to preload
     it at fetchpriority=high. Same diagnosis already fixed for
     .page-banner::before, which the hero was never brought along with.
     Stronger image, and the scrim now varies: dense where the headline and
     lede sit, lighter through the middle, dark again under the bar.
     The mid floor is 0.70, NOT lower. On a phone .hero__actions .btn is
     width:100% and the buttons land in exactly this band; .btn--outline-light
     is a translucent white gradient, so it LIGHTENS whatever is behind it.
     Measured against a pure-white worst-case photo, its white label holds
     4.75:1 at 0.70 but falls to 3.68:1 at 0.58 - under the AA floor for text
     this size. 0.70 still more than doubles how much photograph is visible. */
  .hero::after {
    background: linear-gradient(180deg,
      rgba(10,24,38,0.93) 0%,
      rgba(10,24,38,0.90) 44%,
      rgba(10,24,38,0.70) 76%,
      rgba(10,24,38,0.84) 100%);
  }
  .hero::before { opacity: 0.80; background-position: 70% center; }
  /* The matching .hero__bar override is NOT here - see below .hero__bar. */
}
/* z-index 2, not 1: the scrim ::after also sits at z-index 1 and paints
   later in DOM order, so content at z-index 1 renders BEHIND the scrim and
   the headline goes dim. This selector (0,2,0) also outranks .hero__inner
   (0,1,0), so it is the one that actually decides the stacking. */
.hero > .wrap { position: relative; z-index: 2; }
/* 3.05rem, not 3.5rem: at 1440px the headline wrapped to four lines with only
   two words on the first, because the type was too large for the column it
   sits in - and it pushed the primary CTA's bottom edge to y=702 on a 900px
   viewport. 48.8px in a 736px column is three lines, still entirely inside
   the >=0.94-opacity part of the desktop scrim. */
.hero h1 { color: #fff; font-size: clamp(2.1rem, 3.1vw + 1rem, 3.05rem); }
.hero .lede { color: rgba(255,255,255,0.82); font-size: var(--fs-lg); margin-top: 1.3rem; max-width: 60ch; }
.hero__actions { display: flex; flex-wrap: wrap; gap: 1rem; margin-top: 2.4rem; }

/* A row of compact jump chips inside a banner. Its own class rather than
   borrowing .hero__actions, which drags in `width: 100%` on its buttons below
   560px (see the responsive block at the foot of this file) - four chips then
   became a full-width stack about 690px tall and pushed the first section
   below the fold on a phone. These wrap inline instead, at every width.
   Used by the /services banner; see the note beside that nav. */
.banner-jump { display: flex; flex-wrap: wrap; gap: 0.6rem; margin-top: 2.4rem; }

/* Text column is capped so lines stay readable while the photograph fills
   the rest of the frame. */
.hero__inner { position: relative; z-index: 2; }
.hero__content { max-width: 46rem; }

/* Full-width credential strip closing the hero. Replaces the loose floating
   dot list, which read as an afterthought rather than part of the design. */
.hero__bar {
  position: relative;
  z-index: 2;
  margin-top: clamp(2.5rem, 5vw, 4rem);
  border-top: 1px solid rgba(255,255,255,0.13);
  background: rgba(0,0,0,0.14);
}
.hero__bar-inner {
  display: flex;
  flex-wrap: wrap;
  gap: 0.85rem 2.75rem;
  padding-block: 1.15rem;
  color: rgba(255,255,255,0.72);
  font-size: var(--fs-sm);
}
.hero__bar-inner span { display: inline-flex; align-items: center; gap: 0.6em; }
.hero__bar-inner span::before {
  content: "";
  width: 6px; height: 6px;
  border-radius: 50%;
  background: var(--color-accent-light);
  display: inline-block;
  flex-shrink: 0;
}
/* Belongs with the mobile hero scrim block above, and it CANNOT live there:
   `.hero__bar` is declared at (0,1,0) further down than that block, a media
   query adds no specificity, and so the base `rgba(0,0,0,0.14)` would win and
   this override would silently never apply. Declared after the base rule
   instead. With the photograph now at 0.80 opacity on mobile, a 0.14 black
   wash no longer separates the credential strip from the image behind it. */
@media (max-width: 860px) {
  .hero__bar { background: rgba(10,24,38,0.62); }
}

/* ---- Page banner (inner-page header with photograph) ----
   Same defensive layering as the hero: navy gradient is the base, so a
   missing image degrades to a solid branded band rather than a blank box. */
.page-banner {
  position: relative;
  overflow: hidden;
  background:
    radial-gradient(900px 400px at 80% 0%, rgba(var(--color-accent-rgb),0.18), transparent 60%),
    var(--gradient-navy);
  color: #fff;
  padding-block: clamp(3rem, 6vw, 5rem);
}
/* 0.42 opacity under a 55% scrim left the photographs almost invisible on the
   right-hand side - the banner read as a plain navy band with a smudge. The
   image is now much stronger, and the scrim does the legibility work only
   where the text actually sits (the left). */
.page-banner::before {
  content: "";
  position: absolute;
  inset: 0;
  background-size: cover;
  background-position: center;
  opacity: 0.75;
  z-index: 0;
}
.page-banner::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg, rgba(10,24,38,0.96) 0%, rgba(10,24,38,0.88) 38%, rgba(10,24,38,0.45) 72%, rgba(10,24,38,0.22) 100%);
  z-index: 1;
}
@media (max-width: 860px) {
  /* Text spans the full width on mobile, so the scrim must be even. */
  .page-banner::after {
    background: linear-gradient(180deg, rgba(10,24,38,0.88) 0%, rgba(10,24,38,0.82) 100%);
  }
}
.page-banner > .wrap { position: relative; z-index: 2; }
.page-banner h1 { color: #fff; }
.page-banner .lede { color: rgba(255,255,255,0.85); margin-top: 1.1rem; max-width: 62ch; }
.page-banner .eyebrow { color: var(--color-accent-light); }

/* Each banner declares the JPEG first and then the image-set - see the note on
   .hero::before for why the order is load-bearing. These five are the LCP
   element on every page that is not the home page, and they were 173-256 KB
   each; the WebP siblings are 56-95 KB. */
.page-banner--services::before {
  background-image: url('/assets/img/banner-services.jpg');
  background-image: image-set(url('/assets/img/banner-services.webp') type('image/webp'),
                              url('/assets/img/banner-services.jpg') type('image/jpeg'));
}
.page-banner--industries::before {
  background-image: url('/assets/img/banner-industries.jpg');
  background-image: image-set(url('/assets/img/banner-industries.webp') type('image/webp'),
                              url('/assets/img/banner-industries.jpg') type('image/jpeg'));
}
.page-banner--about::before {
  background-image: url('/assets/img/banner-about.jpg');
  background-image: image-set(url('/assets/img/banner-about.webp') type('image/webp'),
                              url('/assets/img/banner-about.jpg') type('image/jpeg'));
}
.page-banner--faq::before {
  background-image: url('/assets/img/banner-faq.jpg');
  background-image: image-set(url('/assets/img/banner-faq.webp') type('image/webp'),
                              url('/assets/img/banner-faq.jpg') type('image/jpeg'));
}
/* The only banner with no position override, so it fell back to `center` and
   the dog was cut in half by the band's bottom edge. */
.page-banner--contact::before {
  background-image: url('/assets/img/banner-contact.jpg');
  background-image: image-set(url('/assets/img/banner-contact.webp') type('image/webp'),
                              url('/assets/img/banner-contact.jpg') type('image/jpeg'));
  background-position: 82% 72%;
}

/* Privacy, Terms and Sitemap get the same masthead so the site does not change
   shape when you reach them, but there is no photograph for these pages and
   inventing one would be filler. The scrim (::after) exists only to keep text
   legible over a photo, so with no photo it is switched off entirely and the
   navy gradient stands on its own - otherwise it darkened the band for no
   reason. Slightly shorter too, since there is no image to give room to. */
.page-banner--plain { padding-block: clamp(2.5rem, 4.5vw, 3.75rem); }
.page-banner--plain::before,
.page-banner--plain::after { content: none; }

/* Phones and small tablets were downloading the full 2400px masthead art -
   between 170 KB and 250 KB each, on exactly the connections least able to
   afford it. These are 1200px re-encodes (45-75 KB) and they sit behind a
   scrim at 0.55-0.75 opacity, where the difference is not perceptible.
   Kept as a separate block, after the full-size rules, so source order does
   the overriding and no specificity bump is needed. */
@media (max-width: 860px) {
  /* Both declarations are repeated per rule here, exactly as in the desktop
     block. A single `background-image: image-set(...)` would be enough for a
     modern browser, but the plain url() has to come with it: an engine that
     drops the image-set falls back to the LAST declaration it understood,
     which without this line is the 2400px DESKTOP jpg - so the one class of
     device this whole block exists to protect would download the largest file
     on the site. */
  .hero::before {
    background-image: url('/assets/img/hero-detection-team-1200.jpg');
    background-image: image-set(url('/assets/img/hero-detection-team-1200.webp') type('image/webp'),
                                url('/assets/img/hero-detection-team-1200.jpg') type('image/jpeg'));
  }
  .page-banner--services::before {
    background-image: url('/assets/img/banner-services-1200.jpg');
    background-image: image-set(url('/assets/img/banner-services-1200.webp') type('image/webp'),
                                url('/assets/img/banner-services-1200.jpg') type('image/jpeg'));
  }
  .page-banner--industries::before {
    background-image: url('/assets/img/banner-industries-1200.jpg');
    background-image: image-set(url('/assets/img/banner-industries-1200.webp') type('image/webp'),
                                url('/assets/img/banner-industries-1200.jpg') type('image/jpeg'));
  }
  .page-banner--about::before {
    background-image: url('/assets/img/banner-about-1200.jpg');
    background-image: image-set(url('/assets/img/banner-about-1200.webp') type('image/webp'),
                                url('/assets/img/banner-about-1200.jpg') type('image/jpeg'));
  }
  .page-banner--faq::before {
    background-image: url('/assets/img/banner-faq-1200.jpg');
    background-image: image-set(url('/assets/img/banner-faq-1200.webp') type('image/webp'),
                                url('/assets/img/banner-faq-1200.jpg') type('image/jpeg'));
  }
  .page-banner--contact::before {
    background-image: url('/assets/img/banner-contact-1200.jpg');
    background-image: image-set(url('/assets/img/banner-contact-1200.webp') type('image/webp'),
                                url('/assets/img/banner-contact-1200.jpg') type('image/jpeg'));
  }
}

/* This block MUST sit after the desktop `.page-banner--contact::before` rule,
   not in the generic mobile block further up. Both are (0,1,1) and a media
   query adds no specificity, so a mobile override written earlier in the file
   loses to the desktop rule and silently never applies - the same failure that
   put a supporting card above its own heading once already.
   At 500px a pixel scan across this band returned nothing but navy: the
   generic 0.88/0.82 mobile scrim erased a 50 KB image that is still preloaded
   at high priority. Scoped lighter scrim, and the crop pulled toward the dog.
   Worst case (a pure-white photograph) the h1 holds 8.5:1, the lede 6.7:1 and
   the accent-light phone link 4.6:1. */
@media (max-width: 860px) {
  .page-banner--contact::before { background-position: 88% bottom; }
  .page-banner--contact::after {
    background: linear-gradient(180deg, rgba(10,24,38,0.78), rgba(10,24,38,0.70));
  }
}

/* ---- Media card (image + copy) ---- */
.media-figure {
  position: relative;
  border-radius: var(--radius-lg);
  overflow: hidden;
  border: 1px solid var(--color-border);
  box-shadow: var(--shadow-md);
  background: var(--gradient-navy);
  aspect-ratio: 4 / 3;
}
.media-figure img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform 0.7s var(--ease-out);
}
.media-figure:hover img { transform: scale(1.04); }
.media-figure figcaption {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  padding: 1.5rem 1.35rem 1.1rem;
  background: linear-gradient(180deg, transparent, rgba(10,24,38,0.92));
  color: #fff;
  font-size: var(--fs-sm);
  line-height: 1.5;
}

/* Set by main.js when an image 404s, so a missing photo reads as a designed
   navy panel instead of the browser's broken-file icon. */
.is-imageless img { display: none; }
.is-imageless {
  background:
    radial-gradient(420px 200px at 70% 20%, rgba(var(--color-accent-rgb),0.22), transparent 65%),
    var(--gradient-navy);
}

/* Card with a photo header */
/* .card.card--media, not .card--media: both are (0,1,0) and `.card` is
   declared LATER in this file, so the single-class version lost on source
   order and every media card carried 32px of white gutter round its photo. */
.card.card--media { padding: 0; overflow: hidden; }
.card--media .card__media { aspect-ratio: 16 / 10; overflow: hidden; background: var(--gradient-navy); }
.card--media .card__media img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform 0.7s var(--ease-out); }
.card--media:hover .card__media img { transform: scale(1.05); }
.card--media .card__body { padding: 1.6rem 1.75rem 1.9rem; }
.card--media .card__body h3 { margin-bottom: 0.5rem; }
/* Bottom-align the "How ... works" link so the three cards in an equal-height
   row end on one line instead of three (57px/32px/32px of slack, measured on
   the home page). `height: 100%` does NOT work here - .card sets no height, so
   the percentage has nothing to resolve against; a flex column does.
   padding-top, not just `margin-top: auto`: on the TALLEST card there is no
   free space for auto to absorb, so the link would sit flush against the
   paragraph above it. The padding keeps the original 1.1rem gap in that case
   and enlarges the link's own hit area in every other. */
.card.card--media { display: flex; flex-direction: column; }
.card--media .card__body { flex: 1; display: flex; flex-direction: column; }
.card--media .card__link { margin-top: auto; padding-top: 1.1rem; }
/* Makes the WHOLE card the click target. The card already lifts and glows on
   hover, but only the 23px-tall text link was clickable - one pixel under the
   WCAG 2.2 SC 2.5.8 minimum, and a promise the card visibly failed to keep.
   .card is position: relative and .card__link is static, so this resolves
   against the card box. :not(.card--flush) is deliberate - the aside cards on
   /services and /industries use the media card as a static panel, and must
   not silently become whole-card links. Text selection inside the card
   becomes drag-only, which is the accepted cost. */
.card--media:not(.card--flush) .card__link::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
}

/* ---- Cards ---- */
.card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  padding: 2rem;
  box-shadow: var(--shadow-sm);
  transition: box-shadow var(--transition-med), transform var(--transition-med), border-color var(--transition-med);
  position: relative;
  overflow: hidden;
}
/* Accent rule that wipes in along the card's top edge on hover. */
/* Hover feedback ONLY on cards that actually do something. Most .card
   elements on this site are static content panels; lifting and glowing them
   promises a click that never happens. :has() scopes it to cards containing
   a link; browsers without :has() simply get no hover, which is correct. */
.card:has(a)::before {
  content: "";
  position: absolute;
  top: 0; left: 0;
  height: 3px;
  width: 0;
  background: var(--gradient-accent);
  transition: width var(--transition-med);
}
.card:has(a):hover::before { width: 100%; }
.card:has(a):hover {
  box-shadow: var(--shadow-md);
  transform: translateY(-4px);
  border-color: rgba(var(--color-accent-rgb),0.4);
}
/* The contact sidebar is four visually identical static panels. One of them
   happens to contain a phone link, so :has(a) singled it out with a lift and
   an accent bar - promising a click the CARD does not accept. The phone number
   inside keeps its own .contact-aside__phone:hover, which is the real
   affordance. (0,2,1) and (0,3,1) beat the base rules above outright. */
.contact-aside .card:has(a)::before { content: none; }
.contact-aside .card:has(a):hover {
  transform: none;
  box-shadow: var(--shadow-sm);
  border-color: var(--color-border);
}
/* No rotate. A tilting icon reads as playful, which is the wrong register
   for a security vendor being evaluated by a school board. */
.card h3 { margin-bottom: 0.55rem; }
.card p { color: var(--color-ink-muted); font-size: var(--fs-md); }
.card__link {
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  margin-top: 1.1rem;
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--color-navy);
  text-decoration: none;
  transition: gap var(--transition-fast), color var(--transition-fast);
}
.card__link:hover { color: var(--color-accent-dark); gap: 0.75em; }
/* No height:100%. Forcing these to match the (much taller) text column left
   them 40-70% empty - the card looked like content had failed to load. They
   now size to their own content, which .grid--2's align-items:start allows. */
.card--flush { align-self: start; }
.card h4 { margin-bottom: 0.85rem; }

.pillar-list { display: grid; gap: 0.5rem; }
.pillar {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 1.4rem;
  padding-block: 1.6rem;
  border-top: 1px solid rgba(255,255,255,0.13);
  transition: border-color var(--transition-med);
}
.pillar:first-child { border-top: none; }
.pillar:hover { border-color: rgba(var(--color-accent-rgb),0.5); }
/* Every .pillar is its own grid, so the `auto` number column is measured
   per item and never shared - and Georgia sets digits proportionally, so "01"
   asked for a narrower column than "02"/"03"/"04" and the stacked items' text
   edges came out ragged by a few pixels. tabular-nums makes the digits equal
   width; the 2ch floor is the belt to that braces, for a stack that lacks the
   feature. (.step__num sidesteps this with an explicit 40px circle.) */
/* --font-body for the same reason .brand__text is: in Georgia this stack ran
   01 02 at x-height, 03 04 05 hanging below the baseline and 06 rising above
   it - six "numbers" on /industries sharing no baseline and no height, which
   makes a numbered list of capabilities look like a typographic accident
   rather than a sequence. Sans lining figures are all one height and all sit
   on the line. (The tabular-nums and 2ch floor below are still wanted: they
   equalise the COLUMN width across separately-gridded .pillar items.) */
.pillar__num {
  font-family: var(--font-body);
  font-variant-numeric: tabular-nums;
  min-width: 2ch;
  font-size: 1.7rem;
  background-image: var(--gradient-accent);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  font-weight: 700;
}

/* ---- Process steps ---- */
/* Uncapped, every step card spanned the full 1232px container with 390-420px
   of empty space to the right of its own prose - five stretched bars rather
   than a designed column. 900px clears the 74ch cap on .step p (723px) plus
   the 60px number column and 56px of padding.
   (`counter-reset: step` was dead - there is no counter-increment or counter()
   anywhere in this file; the numbers are literal text in the markup.) */
.steps { display: grid; gap: 1.25rem; max-width: 900px; margin-inline: auto; }
.step {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 1.25rem;
  align-items: start;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  padding: 1.5rem 1.75rem;
  box-shadow: var(--shadow-sm);
  transition: border-color var(--transition-med);
}
/* No step card contains a link, so a lift-and-glow promised interactivity
   that is not there - the same problem `.card:has(a)` was written to solve.
   A quiet border response only. */
.step:hover { border-color: rgba(var(--color-accent-rgb),0.35); }
.step__num {
  width: 40px; height: 40px;
  flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  border-radius: 50%;
  background-image: var(--gradient-navy);
  color: #fff;
  font-weight: 700;
  font-size: var(--fs-md);
  font-family: var(--font-body);
}
.step h3 { font-size: var(--fs-lg); margin-bottom: 0.35rem; }
/* Capped on the paragraph itself, where `ch` resolves against this font-size
   rather than the parent's. Uncapped, a step card spanning the full container
   put ~150 characters on a line (161 on /about); the comfortable range is
   45-85. The CARD still spans the full width - only the prose is limited. */
.step p { color: var(--color-ink-muted); font-size: var(--fs-md); max-width: 74ch; }
/* On a phone the number circle, gap and padding ate 116px of a 342px
   container, leaving a 209px text column - about 27 characters per line.
   Same single-class specificity as the base rules above, later in source. */
@media (max-width: 600px) {
  .step { padding: 1.25rem 1.15rem; gap: 0.85rem; }
  .step__num { width: 34px; height: 34px; font-size: var(--fs-sm); }
}

/* ---- Who we serve strip ---- */
/* `display: grid` so the strip can be marked up as a real <ul>/<li> without
   the tiles going ragged - the <li> becomes the stretching grid item and the
   anchor fills it. Inert until that markup lands. */
.serve-grid li { display: grid; }
.serve-grid a {
  display: block;
  height: 100%;
  position: relative;
  /* Right padding is wider than the left to clear the chevron below. */
  padding: 1.6rem 2.8rem 1.6rem 1.4rem;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  text-decoration: none;
  color: var(--color-navy);
  box-shadow: var(--shadow-sm);
  transition: border-color var(--transition-med), box-shadow var(--transition-med), transform var(--transition-med);
}
.serve-grid a:hover { border-color: var(--color-accent); box-shadow: var(--shadow-md); transform: translateY(-4px); }
/* The only signal these tiles were clickable was a hover lift, which does not
   exist on touch - so on a phone the whole row read as static blurbs. Site
   precedent for a marker drawn in CSS rather than typed into the markup is
   .plain-list a::before. */
.serve-grid a::after {
  content: "";
  position: absolute;
  right: 1.4rem;
  top: 1.85rem;
  width: 8px;
  height: 8px;
  border-right: 2px solid var(--color-accent);
  border-top: 2px solid var(--color-accent);
  transform: rotate(45deg);
  transition: transform var(--transition-fast);
}
.serve-grid a:hover::after { transform: rotate(45deg) translate(2px, -2px); }
/* min-height is exactly two lines at this line-height, so a title that wraps
   and one that does not still hand their description the same baseline. It was
   25px out across a five-across row, which put the second line of body text on
   three different baselines. */
.serve-grid strong {
  display: block;
  font-weight: 700;
  font-size: var(--fs-md);
  margin-bottom: 0.35rem;
  line-height: 1.3;
  min-height: 2.6em;
}
/* The reservation above is for ALIGNING titles across a row. At 420px .grid--5
   is a single column (line 302), so there is no row left to align to - every
   title fits on one line and the second reserved line is ~22px of dead space
   under all five tiles, on / and /industries. Same single-class specificity,
   later in source, so it wins where it applies. */
@media (max-width: 420px) {
  .serve-grid strong { min-height: 0; }
}
.serve-grid span { display: block; font-size: var(--fs-sm); color: var(--color-ink-muted); line-height: 1.5; }

/* ---- FAQ (accordion) ---- */
/* The measure cap belongs HERE, on the group, not on <summary>. Capping the
   summary capped the CLICK TARGET: the right ~226px of every question row
   looked like part of the card but did nothing, and the chevron - positioned
   against the summary - floated 253px inside the card's own edge. Narrowing
   the group narrows the cards themselves, so the whole row stays clickable. */
.faq-group { max-width: 62rem; margin-inline: auto; }
.faq-group + .faq-group { margin-top: 3rem; }
/* These headings were --fs-xl, only 3.2px larger than the questions under
   them and 14.4px SMALLER than the page's other h2, and their 2px rule used
   --color-border, which is ~1.25:1 on the page background - invisible. The
   page read as one undifferentiated stack. (0,1,0) still beats the bare h2. */
.faq-group__title {
  font-size: clamp(1.5rem, 1vw + 1.15rem, 1.9rem);
  margin-bottom: 1.1rem;
  padding-bottom: 0.6rem;
  border-bottom: 2px solid rgba(var(--color-accent-rgb),0.35);
}
.faq-list { display: grid; gap: 0.85rem; }
.faq-item {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  overflow: hidden;
  transition: box-shadow var(--transition-med), border-color var(--transition-med);
}
.faq-item[open] { box-shadow: var(--shadow-md); border-color: rgba(var(--color-accent-rgb),0.4); }
/* Deep-linked questions must clear the sticky header. */
.faq-item { scroll-margin-top: 6rem; }
.faq-item:target { border-color: var(--color-accent); box-shadow: var(--shadow-md); }
.faq-item summary {
  cursor: pointer;
  list-style: none;
  padding: 1.35rem 3.5rem 1.35rem 1.75rem;
  font-family: var(--font-heading);
  font-size: var(--fs-lg);
  font-weight: 600;
  color: var(--color-navy);
  position: relative;
  transition: color var(--transition-fast);
  /* A double-click on a question toggled it twice (back to where it started)
     AND selected the text, so it read as a broken control. The answer stays
     selectable - only the question label is not. */
  -webkit-user-select: none;
  user-select: none;
}
.faq-item summary::-webkit-details-marker { display: none; }
/* .faq-item sets overflow:hidden to clip its rounded corners, which would eat
   a positive outline-offset, so this ring is drawn just inside the card. */
.faq-item summary:focus-visible { outline-offset: -3px; border-radius: var(--radius-lg); }
/* The question is an <h3> for the document outline (twelve questions used to
   contribute no headings at all). It must not bring heading typography with
   it - the summary already owns that. */
.faq-item summary h3 {
  font: inherit;
  color: inherit;
  margin: 0;
  display: inline;
}
.faq-item summary:hover { color: var(--color-accent-dark); }
.faq-item summary::after {
  content: "";
  position: absolute;
  right: 1.75rem;
  top: 50%;
  width: 10px; height: 10px;
  border-right: 2px solid var(--color-accent);
  border-bottom: 2px solid var(--color-accent);
  transform: translateY(-70%) rotate(45deg);
  transition: transform var(--transition-med);
}
.faq-item[open] summary::after { transform: translateY(-30%) rotate(-135deg); }
.faq-item__body { padding: 0 1.75rem 1.5rem; }
/* Answers spanned the full 1280px container - roughly 170 characters per
   line, which is unreadable. The card stays full width; the prose inside
   is capped. */
/* 64ch, the same prose measure as .longform p - see the note there for why the
   number is not the ~78 the character count suggests. */
/* Ink, not muted grey. An expanded answer is the primary content of this page,
   not a caption: 16.9:1 against white instead of 5.9:1. It also makes the link
   colour below legible as a link — against muted grey, the teal accent sits at
   1.03:1, i.e. the same luminance, so a link was invisible except in hue. */
.faq-item__body p { color: var(--color-ink); font-size: var(--fs-md); max-width: 64ch; }
/* Prose links inside an answer. Without this, `a { color: inherit }` renders
   an anchor identically to the sentence around it - i.e. invisible - which is
   why answers that tell the reader to go somewhere have never linked there.
   (0,1,1) beats the reset's (0,0,1). No :visited/:active - see the reset.
   (The block that used to sit here prescribed NAVY, on the premise that the
   answer copy is --color-ink-muted. It is not: the rule above sets it to full
   ink, which is the whole reason accent-dark is legible here. The declaration
   was right and the comment described a page that no longer existed.) */
/* Same treatment as `.longform a` — prose links must not be one colour on the
   FAQ and another on the legal pages. Weight 600 is doing real work here, not
   decoration: links on this site are never underlined (explicit requirement),
   and no palette colour reaches the 3:1 against body text that WCAG 1.4.1 asks
   for when colour is the ONLY cue — accent-dark on ink measures 2.98:1. The
   weight difference is therefore the non-colour cue that keeps it conformant.
   Do not "simplify" it away. */
.faq-item__body a { color: var(--color-accent-dark); font-weight: 600; transition: color var(--transition-fast); }
.faq-item__body a:hover { color: var(--color-navy); }

/* ---- Forms ---- */
.form-card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-md);
  padding: clamp(1.5rem, 4vw, 2.75rem);
}
.form-card__head {
  margin-bottom: 1.75rem;
  padding-bottom: 1.25rem;
  border-bottom: 1px solid var(--color-border);
}
.form-card__head h2 { font-size: var(--fs-2xl); margin-bottom: 0.4rem; }
.form-card__head p { color: var(--color-ink-muted); font-size: var(--fs-md); }

.form-grid {
  display: grid;
  /* min() for the same reason as .legal-toc ol and .plain-list: a bare
     minmax(240px, …) floor cannot shrink, so at a 320px viewport the form card
     measured 290px inside a 272px column and /contact was the last page still
     scrolling sideways (WCAG 1.4.10 Reflow, which is specified at 320px). The
     two-column desktop layout is unchanged - the floor only yields once the
     container is narrower than it, which is already single-column territory. */
  grid-template-columns: repeat(auto-fit, minmax(min(240px, 100%), 1fr));
  gap: 1.35rem;
}
.form-field { display: flex; flex-direction: column; gap: 0.45rem; }
.form-field--full { grid-column: 1 / -1; }
.form-field label { font-size: var(--fs-sm); font-weight: 600; color: var(--color-navy); }
.form-field .optional { font-weight: 400; color: var(--color-ink-muted); }
/* The instruction sentence above the fields QUOTES this marker ("Fields marked
   (optional) can be left blank"), but it lives in .form-card__head, which the
   .form-field-scoped rule never reaches - so the quoted token rendered as
   ordinary muted body text and did not visibly match the thing it names.
   Same shape as .legal-toc__title and .card--media: a scoped rule that does
   not reach one of its instances. Here the marker must stand OUT of muted
   prose rather than recede from a bold label, so the values are inverted. */
.form-card__head .optional { font-weight: 600; color: var(--color-navy); }
/* Persistent guidance under a field. Placeholder text disappears on the first
   keystroke, which is exactly when guidance about what to write is needed. */
.field-hint { font-size: var(--fs-sm); color: var(--color-ink-muted); margin: -0.15rem 0 0; }
.form-field input,
.form-field select,
.form-field textarea {
  border: 1.5px solid var(--color-border-input);
  border-radius: var(--radius);
  padding: 0.82em 0.95em;
  background: var(--color-surface);
  font-size: var(--fs-md);
  width: 100%;
  transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
}
.form-field input:focus,
.form-field select:focus,
.form-field textarea:focus {
  border-color: var(--color-accent);
  box-shadow: 0 0 0 4px rgba(var(--color-accent-rgb),0.18);
}
/* Mouse clicks stay ringless; keyboard focus keeps the standard 3px accent
   outline, which is also the one indicator Windows High Contrast preserves.
   `outline: none` unconditionally removed it for keyboard users too. */
.form-field input:focus:not(:focus-visible),
.form-field select:focus:not(:focus-visible),
.form-field textarea:focus:not(:focus-visible) { outline: none; }
.form-field textarea { resize: vertical; min-height: 150px; }
.form-field select {
  appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='9' viewBox='0 0 14 9'%3E%3Cpath d='M1 1l6 6 6-6' fill='none' stroke='%235B6672' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 0.95em center;
  padding-right: 2.4em;
}
.form-field input[aria-invalid="true"],
.form-field select[aria-invalid="true"],
.form-field textarea[aria-invalid="true"] {
  border-color: var(--color-error);
  box-shadow: 0 0 0 4px rgba(180,52,46,0.12);
}
/* `min-height: 0` was a no-op on an inline span. Four fields can fail at once,
   and each message appearing from nothing shoved every field below it down -
   the form jumped under the reader mid-correction. Block-level with the line
   reserved up front, so showing a message changes no geometry. */
.field-error {
  display: block;
  font-size: var(--fs-sm);
  color: var(--color-error);
  font-weight: 500;
  min-height: 1.35em;
  margin-top: 0.25rem;
}

/* Honeypot: visually hidden but present in the DOM/tab order avoided,
   not display:none (some bots skip display:none fields deliberately). */
.field-trap {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

.form-note { margin-top: 1.35rem; font-size: var(--fs-sm); color: var(--color-ink-muted); }
/* Where the form's data goes, stated next to the button that sends it. */
.form-privacy-note {
  margin-top: 1.1rem;
  font-size: var(--fs-sm);
  color: var(--color-ink-muted);
}
/* Both of these notes are --color-ink-muted, and a link has to be tellable
   apart from the sentence it sits in. Measured against #5B6672: navy 2.56:1,
   --color-accent-dark 1.03:1 - the teal is the SAME brightness as muted grey,
   so it is the wrong choice here even though it is right inside .longform,
   where the surrounding copy is full ink (2.98:1). The 600 weight is the
   non-colour cue WCAG 1.4.1 wants, since this site never underlines. Both
   notes sit under the same submit button, so they must match. */
.form-privacy-note a { color: var(--color-navy); font-weight: 600; transition: color var(--transition-fast); }
.form-privacy-note a:hover { color: var(--color-accent-dark); }

.recaptcha-note { margin-top: 1rem; font-size: var(--fs-xs); color: var(--color-ink-muted); }
.recaptcha-note a { color: var(--color-navy); font-weight: 600; transition: color var(--transition-fast); }
.recaptcha-note a:hover { color: var(--color-accent-dark); }

/* Badge hidden per Google's own allowance, on condition that the required
   attribution text (.recaptcha-note above) is visibly shown near the form
   it protects - see https://developers.google.com/recaptcha/docs/faq */
.grecaptcha-badge { visibility: hidden; }

/* ---- Contact reassurance panel ---- */
.contact-layout { display: grid; grid-template-columns: 1.35fr 1fr; gap: 2rem; align-items: start; }
@media (max-width: 980px) { .contact-layout { grid-template-columns: 1fr; } }
.contact-aside { display: grid; gap: 1.25rem; }
.contact-aside .card p { font-size: var(--fs-md); }

/* ---- Thank-you panel (replaces the form on successful submit) ---- */
.thank-you { display: none; text-align: center; padding: 2.5rem 1rem; }
.thank-you.is-visible { display: block; animation: pop 0.45s var(--ease-out); }
.thank-you:focus { outline: none; }
@keyframes pop {
  from { opacity: 0; transform: scale(0.96) translateY(10px); }
  to { opacity: 1; transform: none; }
}
.thank-you__icon {
  width: 72px; height: 72px;
  margin: 0 auto 1.4rem;
  border-radius: 50%;
  background: var(--color-success-bg);
  color: var(--color-success);
  display: flex; align-items: center; justify-content: center;
  box-shadow: var(--shadow-sm);
}
.thank-you h2 { margin-bottom: 0.6rem; }
.thank-you p { color: var(--color-ink-muted); max-width: 52ch; margin-inline: auto; }
/* The panel's recovery links (/faq, /services) were typographically identical
   to the sentence around them. :not(.contact-aside__phone) because this rule
   is (0,1,1) and would otherwise outrank the phone link's own (0,1,0) styling
   and strip it - the same trap .site-nav > a:not(.btn) documents above. */
.thank-you a:not(.contact-aside__phone) {
  color: var(--color-navy);
  font-weight: 600;
  transition: color var(--transition-fast);
}
.thank-you a:not(.contact-aside__phone):hover { color: var(--color-accent-dark); }

/* ---- Toasts ---- */
.toast-container {
  position: fixed;
  top: 1.25rem;
  right: 1.25rem;
  z-index: 500;
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  width: min(370px, calc(100vw - 2rem));
  pointer-events: none;
}
.toast {
  pointer-events: auto;
  display: flex;
  align-items: flex-start;
  gap: 0.8rem;
  background: var(--color-surface);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
  padding: 1rem 1.1rem;
  border-left: 4px solid var(--color-navy);
  animation: toast-in 0.3s var(--ease-out);
}
.toast.is-leaving { animation: toast-out 0.22s ease-in forwards; }
.toast--success { border-left-color: var(--color-success); }
.toast--error { border-left-color: var(--color-error); }
.toast__icon { flex-shrink: 0; margin-top: 0.1rem; }
.toast--success .toast__icon { color: var(--color-success); }
.toast--error .toast__icon { color: var(--color-error); }
.toast__body { flex: 1; font-size: var(--fs-md); }
.toast__body strong { display: block; color: var(--color-navy); margin-bottom: 0.15rem; }
.toast__body span { color: var(--color-ink-muted); }
.toast__close {
  background: none; border: none;
  color: var(--color-ink-muted);
  font-size: var(--fs-lg); line-height: 1;
  padding: 0.1rem; flex-shrink: 0;
  transition: color var(--transition-fast);
}
.toast__close:hover { color: var(--color-ink); }

@keyframes toast-in {
  from { opacity: 0; transform: translateX(28px); }
  to { opacity: 1; transform: translateX(0); }
}
@keyframes toast-out {
  from { opacity: 1; transform: translateX(0); }
  to { opacity: 0; transform: translateX(28px); }
}

/* ---- Long-form legal/content pages ---- */
/* The reset zeroes heading margins and nothing restored a top margin, so
   every h2 sat flush against the paragraph above it on privacy/terms. */
/* Every h2 keeps its top margin, including the first: on these pages the
   first heading follows intro copy, so zeroing it jammed the two together. */
.longform h2 {
  margin-top: 2.75rem;
  margin-bottom: 0.65rem;
  font-size: var(--fs-2xl);
  padding-bottom: 0.5rem;
  border-bottom: 1px solid var(--color-border);
}
/* The hairline has to end where the prose it introduces ends. Uncapped it ran
   the full 1232px container over a 64ch text column of roughly 677px,
   overshooting its own content by ~555px into empty background - ten times on
   /privacy, fifteen on /terms.
   Scoped to the pages that hold a table of contents, which is exactly the two
   numbered legal documents. /sitemap is .longform too, but there each h2
   introduces a full-width multi-column link grid, so capping the rule there
   would create the same mismatch pointing the other way. :has() carries the
   specificity of its argument, so this is (0,2,1) and outranks the base rule
   above; a browser too old to parse it drops this block and keeps today's
   behaviour rather than breaking.
   42rem (672px), NOT the `64ch` used for the paragraphs below: `ch` resolves
   against the ELEMENT's own font, and this heading is 25.6px Georgia while
   .longform p is 16.8px sans - the same 64ch renders about 819px here against
   about 659px there, which is the overshoot itself rather than a fix for it.
   A rem value is the only way to make two different fonts end at one x.
   Comfortably clear of the longest h2 on either page ("3. Important Notice
   About Detection Services", ~563px), so no heading gains a new wrap. */
.longform:has(.legal-toc) h2 { max-width: 42rem; }
.longform h3 { margin-top: 1.75rem; margin-bottom: 0.4rem; font-size: var(--fs-lg); }
/* Body copy here is full ink, not muted. Everything on these pages was
   --color-ink-muted, which flattened them into one grey slab with no
   hierarchy. Slightly larger type also cuts characters-per-line at the wide
   measure these pages deliberately use. */
/* These pages ran ~163 characters per line at desktop - roughly double the
   readable maximum, and against this file's own stated 45-85 range.
   The cap goes on the ELEMENTS, not on .longform: `ch` resolves against the
   element's own computed font-size, so a cap on the container would resolve
   against the container's font instead of the text inside it.
   64ch, NOT the 78ch that looks like the obvious number. `ch` is the advance
   of "0", and in this font stack "0" is 0.613em while the average lowercase
   glyph is only 0.473em - so a `ch` cap buys about 30% more characters than
   its number suggests. Measured on /terms at 1440px: 78ch renders 101
   characters per line, still over the range. 64ch renders 83 (about 75 on a
   Windows font stack, where "0" is narrower). Measure it, do not assume it. */
.longform p, .longform li {
  color: var(--color-ink);
  font-size: var(--fs-base);
  line-height: 1.72;
  max-width: 64ch;
}
/* These two are grid CELLS, not prose. Both are (0,1,1), the same as
   `.longform li`, so they only win by sitting after it - keep them here.
   The exemption matters most for .plain-list: the moment the sitemap's link
   groups become real <ul>/<li>, they start matching `.longform li`. */
.plain-list li, .legal-toc li { max-width: none; }
/* /sitemap's groups are real <ul>/<li> now, and they live inside .longform, so
   they inherited the long-form list treatment: `.longform ul li::before` painted
   a teal dot in front of the arrow every link already draws, plus a 24px indent
   and .longform's 0.55rem gap instead of this component's own. Descendant-scoped
   at (0,2,x) so it outranks `.longform ul li` (0,1,2) - a bare `.plain-list li`
   is (0,1,1) and would lose no matter where it sat in the file. */
.longform .plain-list { gap: 0.85rem 2.5rem; margin: 1rem 0 0; }
.longform .plain-list li { padding-left: 0; }
.longform .plain-list li::before { content: none; }
.longform p + p { margin-top: 1rem; }
/* Intro-paragraph treatment, now OPT-IN. It used to be opt-out
   (`> div:not(.legal-meta) >`), which meant every unclassed div child of
   .longform got it whether or not it was an intro: on /terms that rendered
   clause 1 - the binding "by accessing this site you agree" paragraph - in
   muted 18.4px intro grey while clauses 2-15 sat at full-ink 16.8px. An
   opt-out rule cannot tell an introduction from the first sentence of a
   contract, so the page has to say which it is. */
.longform > .longform-intro > p:first-of-type { font-size: var(--fs-lg); color: var(--color-ink-muted); }
/* list-style: none is required. The global reset only strips markers from
   ul[class], and these lists carry no class, so the native bullet rendered
   alongside the ::before dot below - two markers per item. */
.longform ul {
  margin: 0.9rem 0 1.1rem;
  padding-left: 0;
  list-style: none;
  display: grid;
  gap: 0.55rem;
}
.longform ul:last-child { margin-bottom: 0; }
.longform ul li { position: relative; padding-left: 1.5rem; }
.longform ul li::before {
  content: "";
  position: absolute; left: 0; top: 0.6em;
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--color-accent);
}
.longform a { color: var(--color-accent-dark); font-weight: 600; transition: color var(--transition-fast); }
.longform a:hover { color: var(--color-navy); }
.legal-meta {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-left: 3px solid var(--color-accent);
  border-radius: var(--radius);
  padding: 1.1rem 1.35rem;
  margin-bottom: 2rem;
}
.longform .legal-meta p { font-size: var(--fs-md); margin: 0; }
/* For the one section that says of itself "this section matters more than any
   other on this page" and then renders identically to Severability. Roomier
   than the plain meta box, and its copy stays at full body size and ink.
   The `.longform` prefix on the second rule is MANDATORY: `.legal-meta p` and
   `.longform p` are both (0,1,1), so without it the callout's copy would fall
   to --fs-md and the most important text on the page would end up SMALLER
   than the body around it. (0,2,1) settles it outright. */
.legal-meta--callout { padding: 1.6rem 1.75rem; margin: 1rem 0 2rem; }
.longform .legal-meta--callout p { font-size: var(--fs-base); color: var(--color-ink); }
/* `margin: 0` above is right for the one-paragraph "Last updated" box, but the
   callout holds four paragraphs and at (0,2,1) that rule now outranks
   `.longform p + p` (0,1,2) - so without this the four would run together as
   one slab. (0,2,2) restores the gap. */
.longform .legal-meta--callout p + p { margin-top: 1rem; }

/* Terms runs to 15 numbered sections and Privacy to 10; without this the only
   way to reach the last one is to scroll the whole document. */
.legal-toc {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  padding: 1.5rem 1.75rem;
  margin-bottom: 2.5rem;
}
/* `.legal-toc .legal-toc__title`, not the single class. `.longform p` is
   (0,1,1) and this was (0,1,0), so the eyebrow lost BOTH its size and its
   colour and rendered as ordinary 16.8px ink body text - it did not look like
   a label at all. Same trap as .card--media. */
.legal-toc .legal-toc__title {
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--color-accent-dark);
  margin-bottom: 1rem;
}
/* Was `columns: 2` inside a 1232px card: two 590px column boxes holding
   ~240px of link text, so roughly 350px of the card was dead space. Auto-fit
   packs as many real columns as the width allows and collapses to one on a
   phone without needing its own media query. */
.legal-toc ol {
  list-style: none;
  padding: 0;
  margin: 0;
  display: grid;
  /* 21rem, not 15rem: the longest entry ("3. Important Notice About Detection
     Services") needs 327.6px, and a 15rem floor gave it a 263.5px column at
     1440px. It wrapped, and because grid rows are equal-height that doubled
     the height of the WHOLE first row and left a dead band under entries 1, 2
     and 4. At 21rem the 1174px list yields three 364.7px columns; at 1024px,
     two of 431.5px. 20rem is not enough - it leaves a band around 784-799px
     where two columns of 320-327px still clip. */
  /* min() so the floor yields when the container is narrower than it.
     A bare minmax(21rem, …) cannot shrink below 21rem, which made /privacy
     and /terms 389px wide inside a 375px viewport - a WCAG 1.4.10 reflow
     failure. Desktop behaviour is unchanged. */
  grid-template-columns: repeat(auto-fit, minmax(min(21rem, 100%), 1fr));
  gap: 0.5rem 2.5rem;
}
.legal-toc li { break-inside: avoid; }
/* `.longform .legal-toc a`, not the single class: `.legal-toc a` and
   `.longform a` are both (0,1,1) and this only won on source order. Same tie
   as .legal-toc__title, which lost the identical bet once already. */
.longform .legal-toc a {
  color: var(--color-navy);
  font-size: var(--fs-md);
  font-weight: 500;
  transition: color var(--transition-fast);
}
.longform .legal-toc a:hover { color: var(--color-accent-dark); }
/* Offsets the sticky header when an anchor is followed. */
.longform h2[id] { scroll-margin-top: 6rem; }

/* ---- Error page ---- */
.error-page {
  min-height: 46vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  text-align: center;
}
.error-page .eyebrow { justify-content: center; }
/* Was 80px at desktop - 54% larger than the cap on the home page's own value
   proposition, for an error message - and it was the one h1 on the site the
   mobile tightening pass at the bottom of this file never covered. This clamp
   lands at 56px desktop and ~33.6px at 375px, so it needs no addition there. */
.error-page__title { font-size: clamp(2.1rem, 4vw + 1rem, 3.5rem); }
/* On navy: `a { color: inherit }` would render these at the paragraph's 78%
   white, indistinguishable from the text around them. */
.error-page__more { font-size: var(--fs-md); }
.error-page__more a { color: var(--color-accent-light); font-weight: 600; }
.error-page__more a:hover { color: #fff; }

/* ---- Simple content lists (sitemap) ---- */
/* Single-column inside an unconstrained 1232px container meant roughly half of
   every row was dead space and 14 links were stretched over ~1600px of scroll.
   Identical fix, and identical diagnosis, to .legal-toc ol above. */
.plain-list {
  display: grid;
  /* Same trap as .legal-toc ol above: a 24rem floor made /faq, /sitemap and
     the 404 page 408px wide on a 375px phone. */
  /* auto-FILL, not auto-fit. auto-fit collapses the tracks an under-filled row
     does not use, so /sitemap's two-item "Legal" group stretched into two
     half-width columns and its second link sat ~208px right of where the same
     column starts in every group above it. auto-fill keeps the empty track, so
     every group on the page shares one set of column positions. The min()
     floor still collapses it to a single column on a phone. */
  grid-template-columns: repeat(auto-fill, minmax(min(24rem, 100%), 1fr));
  gap: 0.85rem 2.5rem;
  margin-top: 1rem;
}
/* Grid, not inline-flex: these links contain a block-level description
   <span>, which an inline-flex parent forced onto the same row as the link
   text and squashed. Grid gives the arrow its own column and lets the
   label and description stack properly in the second. */
/* `:any-link` rather than the bare `a`: this was another (0,1,1) tie with
   `.longform a` decided only by source order. All fourteen of these carry an
   href, so the match set is unchanged and (0,2,1) settles it. */
.plain-list a:any-link {
  color: var(--color-navy);
  text-decoration: none;
  font-weight: 600;
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: start;
  gap: 0.1rem 0.6rem;
  padding: 0.55rem 0.7rem;
  margin-left: -0.7rem;
  border-radius: var(--radius);
  transition: color var(--transition-fast), background-color var(--transition-fast);
}
.plain-list a:any-link::before {
  content: "\2192";
  /* The arrow is decoration, but generated content on the ANCHOR is part of
     its accessible name, so all 17 sitemap links, the 4 FAQ jump links and the
     6 on /404 announced as "right arrow Home". The site's own convention
     elsewhere is <span aria-hidden="true">&rarr;</span> precisely to avoid
     that. `/ ""` is the alt-text form of `content`: same glyph, empty string
     for assistive tech. Declared twice on purpose - a browser that does not
     parse the alt-text syntax discards this second declaration and keeps the
     plain arrow above rather than dropping the marker entirely. */
  content: "\2192" / "";
  color: var(--color-accent);
  grid-row: 1;
  line-height: 1.6;
}
.plain-list a:any-link:hover { color: var(--color-accent-dark); background: rgba(var(--color-accent-rgb),0.07); }
.plain-list span {
  grid-column: 2;
  font-size: var(--fs-sm);
  color: var(--color-ink-muted);
  font-weight: 400;
  line-height: 1.5;
}

/* ---- Footer ---- */
/* No margin-top. It exposed a band of page background above the footer -
   most obvious on the 404 and other navy-ending pages, where it cut a grey
   stripe between two dark areas. Sections carry their own bottom padding. */
.site-footer {
  background: var(--gradient-navy);
  color: rgba(255,255,255,0.75);
  position: relative;
  overflow: hidden;
}
.site-footer::before {
  content: "";
  position: absolute;
  inset: 0;
  background: radial-gradient(800px 360px at 10% 0%, rgba(var(--color-accent-rgb),0.14), transparent 55%);
  pointer-events: none;
}
.site-footer__inner {
  display: flex;
  flex-wrap: wrap;
  gap: 2.5rem;
  justify-content: space-between;
  padding-block: 3.5rem 2.5rem;
  position: relative;
}
.site-footer__brand { max-width: 340px; }
.site-footer .brand__text, .site-footer .brand__accent { color: #fff; }
.site-footer .brand__accent { background: none; -webkit-background-clip: unset; background-clip: unset; color: var(--color-accent-light); }
.site-footer__brand p { color: rgba(255,255,255,0.65); font-size: var(--fs-sm); margin-top: 0.85rem; line-height: 1.65; }
/* .site-footer__social and .social-chip are gone with the markup they styled -
   five circular chips that looked exactly like working social icons and were
   <span>s, because none of the accounts exists. Do not re-add the chip style
   speculatively: the day an account is real it gets an <a href> and can have
   its style back then.
   .visually-hidden below is NOT part of that removal - contact.php uses it for
   an aside's heading. */
.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* Two classes (0,2,0) for exactly the reason spelled out on the service-area
   rule below, checked before this was written rather than after it shipped:
   `.site-footer__brand p` (0,1,1) already sets color, font-size AND a 0.85rem
   top margin on every <p> in this column, so a bare `.site-footer__availability`
   (0,1,0) would have lost all three and this rule would have been dead on
   arrival - invisible in review, because the inherited values look plausible.
   Brighter than the service-area line (0.72 vs 0.55) and tucked tight under the
   phone number: it qualifies that number rather than being another line of
   footer boilerplate, and it must not be the quietest thing in the footer. */
.site-footer__brand .site-footer__availability {
  margin-top: 0.5rem;
  font-size: var(--fs-sm);
  color: rgba(255,255,255,0.72);
  line-height: 1.6;
}

/* `.site-footer__brand .site-footer__area`, not the single class. This element
   is also matched by `.site-footer__brand p` (0,1,1) further up the file, which
   outranked the bare `.site-footer__area` (0,1,0) on every one of its four
   declarations - the rule was entirely dead. (0,2,0) settles it. Same trap as
   .legal-toc__title and .card--media, twice already in this file.
   The dimmer 0.55 white is deliberate - this line is the quietest thing in the
   footer - and it still clears WCAG AA on the navy gradient. */
.site-footer__brand .site-footer__area {
  margin-top: 1.1rem;
  font-size: var(--fs-sm);
  color: rgba(255,255,255,0.55);
  line-height: 1.6;
}

.site-footer__cols { display: flex; flex-wrap: wrap; gap: 2.5rem 3.5rem; }
/* A <p>, not an <h2>. These labels are navigation furniture; as headings they
   injected three same-level entries into every page's outline, so a screen
   reader's heading list ended each page with Company / Services / Resources
   ranked alongside the page's own sections. Each nav is still named by its
   label via aria-labelledby, so nothing is lost. */
.site-footer__col-label {
  color: #fff;
  font-size: var(--fs-sm);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  font-family: var(--font-body);
  font-weight: 700;
  margin-bottom: 1.1rem;
}
.site-footer__col nav { display: flex; flex-direction: column; gap: 0.7rem; }
.site-footer__col a {
  color: rgba(255,255,255,0.72);
  text-decoration: none;
  font-size: var(--fs-md);
  transition: color var(--transition-fast), transform var(--transition-fast);
  width: fit-content;
}
.site-footer__col a:hover { color: var(--color-accent-light); transform: translateX(3px); }

.site-footer__legal {
  border-top: 1px solid rgba(255,255,255,0.12);
  padding-block: 1.35rem;
  font-size: var(--fs-sm);
  position: relative;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem 1.5rem;
  justify-content: space-between;
}
/* These <p>s are flex siblings on ONE row, so the global `p + p` top margin
   applied to the 2nd and 3rd and dropped them below the first - the footer
   read as two mismatched baselines. Flex gap handles the spacing here. */
.site-footer__legal p + p { margin-top: 0; }
/* Explicit, not inherited from .site-footer__legal's own color - see the
   note above .section--navy for why a bare `color` on a container never
   reaches its child <p> elements on this site. */
.site-footer__legal p { color: rgba(255,255,255,0.58); }
/* Replaces .site-footer__version in the right-hand slot of the legal bar; see
   the note in includes/footer.php for why a build number had no business
   being the last thing on every page.
   0.72 rather than the 0.58 of the copyright beside it: this is a live mailto
   and the one address on the page, so it must not be the quietest thing in the
   bar. `.site-footer__legal .site-footer__contact` (0,2,0) because
   `.site-footer__legal p` (0,1,1) already sets colour on every <p> in here and
   would beat the bare single class outright - the trap this file has now
   documented for .legal-toc__title, .card--media and .site-footer__area. */
.site-footer__legal .site-footer__contact { color: rgba(255,255,255,0.72); }
/* The <a> inherits from the <p> above via `a { color: inherit }`, so this
   hover is the only rule that ever touches the link's own colour - it wins
   over an inherited value at any specificity. */
.site-footer__legal a:hover { color: var(--color-accent-light); }

/* ---- Vendor band ----
   The slim strip between <main> and the footer, rendered by includes/footer.php
   on every page except the two legal documents, the sitemap, the 404 and the
   two pages it points at. Deliberately quiet: it is a signpost for the one
   visitor who needs it (a purchasing officer), not a second call to action
   competing with the closing CTA immediately above it. Hence .btn--outline and
   not .btn--accent - see the rank rule on .btn--accent. */
.vendor-band {
  background: var(--color-surface);
  border-top: 1px solid var(--color-border);
}
.vendor-band__inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 1rem 2.5rem;
  padding-block: 1.35rem;
}
/* A <p>, not a heading. It labels a piece of site furniture that repeats on
   every page; as an <h2> it would inject the same entry into the heading
   outline of the whole site, which is exactly why .site-footer__col-label is
   a <p> too. The <aside> is named from it via aria-labelledby. */
.vendor-band__title {
  font-family: var(--font-heading);
  font-size: var(--fs-lg);
  font-weight: 600;
  color: var(--color-navy);
  line-height: 1.25;
}
/* No measure cap. A 72ch cap was tried and broke this sentence over two lines
   at 1440px while roughly 340px of the row sat empty beside it - the flex row
   and the 1280px container already bound the line length, and one 110-character
   sentence is not prose that needs a reading measure. */
.vendor-band__items {
  margin-top: 0.3rem;
  font-size: var(--fs-md);
  color: var(--color-ink-muted);
}
/* flex-shrink: 0 so the button keeps its 48px x full label when the sentence
   beside it is long; the sentence wraps first, which is the correct order. */
.vendor-band .btn { flex-shrink: 0; }

/* ---- Spec table (term / value rows) ----
   For the pages a buyer reads with a pen in hand: vendor details, coverage by
   county, what a scope of work has to specify. Plain <table> markup, because
   these ARE two-column data and a definition list cannot be scanned down a
   value column.

   Expected markup - and note there is deliberately no "pending" or "TBC"
   modifier. If a fact is not confirmed the ROW IS OMITTED. A spec table that
   prints "Insurance: to be confirmed" tells a purchasing officer the vendor is
   not ready, which is worse than the row not existing:

     <h3>Vendor Details</h3>
     <table class="spec-table">
       <tbody>
         <tr><th scope="row">Coverage</th><td>The entire United States</td></tr>
         <tr><th scope="row">Availability</th><td>24 hours a day, by appointment</td></tr>
       </tbody>
     </table>

   Put a real heading above it rather than relying on <caption> (the site's own
   convention, and it survives the mobile stack below cleanly). A <caption> is
   styled anyway, for the case where one is genuinely wanted.
   Safe inside .longform: `.longform p`/`li` cap the measure at 64ch and match
   neither <th> nor <td>, so the table keeps its full width. */
.spec-table {
  width: 100%;
  /* separate, not collapse: border-radius is ignored on a collapsed table, so
     the component could not match .card / .legal-meta without it. */
  border-collapse: separate;
  border-spacing: 0;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  margin: 1.25rem 0 1.75rem;
}
.spec-table caption {
  caption-side: top;
  text-align: left;
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--color-accent-dark);
  padding-bottom: 0.7rem;
}
.spec-table th,
.spec-table td {
  text-align: left;
  vertical-align: top;
  padding: 0.85rem 1.25rem;
  font-size: var(--fs-md);
  line-height: 1.6;
  border-top: 1px solid var(--color-border);
}
/* The rule between rows must not appear ABOVE the first one, where it would
   cut across the table's own rounded top corners. */
.spec-table tr:first-child th,
.spec-table tr:first-child td { border-top: 0; }
.spec-table th {
  /* --font-body, not the inherited heading serif: these labels routinely
     contain numerals (dates, counts, form numbers) and Georgia has no lining
     figures - see the note on --font-heading. */
  font-family: var(--font-body);
  font-weight: 700;
  color: var(--color-navy);
  width: 34%;
}
.spec-table td { color: var(--color-ink); }
/* Two 150px columns is not a table, it is two squeezed paragraphs. Below this
   width each row becomes a label stacked on its value.
   Declared after the base rules at the same (0,1,1)/(0,2,2) specificity, so
   source order does the overriding - a media query adds none. */
@media (max-width: 620px) {
  .spec-table, .spec-table caption, .spec-table tbody,
  .spec-table tr, .spec-table th, .spec-table td { display: block; width: auto; }
  .spec-table th { border-top: 1px solid var(--color-border); padding-bottom: 0.1rem; }
  .spec-table td { border-top: 0; padding-top: 0.15rem; }
  .spec-table tr:first-child th { border-top: 0; }
}

/* ---- Capability sheet ----
   The one-page document a school district or municipal buyer asks for by name
   and attaches to a requisition. It is a WEB page that has to print as a
   single clean sheet, so the print rules at the foot of this file are half the
   component - see them before changing anything here.

   Expected markup:

     <section class="capability-sheet">
       <div class="capability-sheet__inner">
         <div class="capability-sheet__head"> mark + company name + one line </div>
         <div class="capability-sheet__grid">
           <div class="capability-sheet__block"><h3>...</h3><p>...</p></div>
           ...
         </div>
         <div class="capability-sheet__foot"> phone, email, website </div>
       </div>
     </section>

   The page should also set $bodyClass = 'sheet-page' in its header block; that
   is what lets the print stylesheet drop the site footer, which no selector
   inside <main> can reach.

   There is NO "Past Performance" block here and none should be added. Every
   capability-statement template on the internet has one, and this company has
   completed no engagements - filling it would be fabrication, and leaving it
   empty announces the gap. */
.capability-sheet { background: var(--color-surface); }
.capability-sheet__inner {
  max-width: 48rem;
  margin-inline: auto;
  padding: clamp(2rem, 4vw, 3rem) 1.5rem;
}
/* Capped well below the global h1 clamp (which tops out at 3.25rem). This is a
   letterhead, not a marketing headline, and at the site's usual h1 size the
   company name alone took three lines of a document that has to fit one page. */
.capability-sheet h1 { font-size: clamp(1.65rem, 2.2vw + 0.9rem, 2.1rem); }
/* The component supplies its own heading rhythm rather than relying on the
   global `h2 + p` pairs: inside the sheet an <h2> follows a grid or a table far
   more often than it follows a paragraph, and those combinations match none of
   the global rules - so the first section heading sat flush against the block
   above it. */
.capability-sheet h2 {
  font-size: var(--fs-2xl);
  margin-top: 2rem;
  margin-bottom: 0.6rem;
}
.capability-sheet__head {
  display: flex;
  align-items: center;
  gap: 1rem;
  padding-bottom: 1.1rem;
  border-bottom: 3px solid var(--color-navy);
}
.capability-sheet__head img { flex-shrink: 0; }
/* On a phone the name wraps to three lines and the mark, vertically centred
   against the whole block, ends up floating beside the middle of a paragraph
   with nothing to align to. Stacked, it reads as a letterhead again.
   Declared here rather than in the responsive block at the foot of the file so
   it sits with the component; both are (0,1,1) and this is later in source than
   the rule it overrides, which is what decides it. */
@media (max-width: 560px) {
  .capability-sheet__head { flex-direction: column; align-items: flex-start; gap: 0.75rem; }
}
.capability-sheet__grid {
  display: grid;
  /* min() floor so a 375px phone gets one column instead of a 240px track
     overflowing its container - the reflow failure this file has already
     fixed on .legal-toc ol and .plain-list. */
  grid-template-columns: repeat(auto-fit, minmax(min(17rem, 100%), 1fr));
  gap: 1.5rem 2.5rem;
  margin-top: 1.75rem;
}
.capability-sheet__block h3 {
  font-size: var(--fs-md);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-accent-dark);
  font-family: var(--font-body);
  font-weight: 700;
  margin-bottom: 0.45rem;
}
.capability-sheet__block p,
.capability-sheet__block li { font-size: var(--fs-md); line-height: 1.6; }
.capability-sheet__foot {
  margin-top: 1.75rem;
  padding-top: 1rem;
  border-top: 1px solid var(--color-border);
  font-size: var(--fs-md);
  color: var(--color-ink-muted);
}
/* Screen-only controls on a document page (the print button). Ships `hidden`
   in the markup and is un-hidden by main.js, so a visitor with no JavaScript
   never sees a button that cannot work. */
.print-actions { display: flex; flex-wrap: wrap; gap: 0.75rem; margin-top: 1.75rem; }

/* ---- Responsive ---- */
@media (max-width: 560px) {
  /* The hero headline ran to five lines and the lede to seven on a 375px
     screen, pushing the CTAs below the fold. Tighter on small viewports. */
  .hero h1 { font-size: 1.95rem; line-height: 1.14; }
  .hero .lede { font-size: 1rem; line-height: 1.55; }
  .page-banner h1 { font-size: 1.85rem; }
  .page-banner .lede, .section-head .lede { font-size: 1rem; }
  .hero__actions .btn, .hero__actions--center .btn { width: 100%; }
  .hero__bar-inner { gap: 0.6rem 1.5rem; font-size: var(--fs-sm); }
}

/* 1000, not 860 and no longer 960. Measured in the browser at 1440px rather
   than estimated: .brand is 262px (the width is set by the "K9 Interdiction &
   Detection Services" expansion, which is white-space: nowrap) and .site-nav
   is 634px, so the masthead needs 944px including the container's 48px of
   padding. The previous 960px threshold left 16px of slack on a macOS font
   stack and NONE on a wider one - Segoe UI runs a few per cent broader, which
   put the collision back inside the desktop range. The old comment's "brand
   119" was measuring the lockup without that expansion.
   This is already better than it was: before the phone number moved out of
   this row the nav was ~715px and the masthead needed ~1025px against the
   same 960px query, so the overlap band was real and shipped.
   The other 860px queries in this file are correct and deliberately left
   alone; .pillar and .toast-container ride along on this one and are
   unaffected by the extra 40px. */
@media (max-width: 1000px) {
  .nav-toggle { display: flex; }
  .site-nav {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    background: var(--color-surface);
    border-bottom: 1px solid var(--color-border);
    box-shadow: var(--shadow-md);
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    padding: 0.5rem 1.5rem 1.25rem;
    display: none;
  }
  .site-nav.is-open { display: flex; animation: nav-in 0.25s var(--ease-out); }
  @keyframes nav-in {
    from { opacity: 0; transform: translateY(-8px); }
    to { opacity: 1; transform: none; }
  }
  .site-nav > a:not(.btn):not(.site-nav__phone) { padding-block: 0.9rem; border-bottom: 1px solid var(--color-border); }
  .site-nav a.btn { margin-top: 0.85rem; justify-content: center; }
  .pillar { grid-template-columns: 1fr; gap: 0.5rem; }
  .toast-container { left: 1rem; right: 1rem; top: 1rem; width: auto; }
}

/* ---- Print ----
   This audience genuinely prints: a school administrator takes the FAQ and
   the Terms into a board packet, and procurement prints the service pages.
   Browsers omit background graphics by default, so without this the hero,
   every navy section and the whole footer print as white-on-white. */
@media print {
  *, *::before, *::after {
    background: transparent !important;
    box-shadow: none !important;
    color: #000 !important;
    text-shadow: none !important;
  }
  body { font-size: 11pt; line-height: 1.45; }
  .site-header, .site-nav, .nav-toggle, .toast-container,
  .hero__bar, .utility-bar, .vendor-band, .print-actions,
  .btn, .grecaptcha-badge,
  .card__media, .media-figure, .page-banner::before, .page-banner::after,
  .hero::before, .hero::after { display: none !important; }

  .hero, .page-banner, .section--navy, .section--surface {
    padding-block: 0.6cm !important;
    border: none !important;
  }
  h1 { font-size: 20pt; }
  h2 { font-size: 15pt; margin-top: 0.7cm; }
  h3 { font-size: 12pt; }
  h1, h2, h3, h4 { page-break-after: avoid; break-after: avoid; }
  p, li { orphans: 3; widows: 3; }
  .card, .step, .faq-item, .form-card {
    border: 1px solid #999 !important;
    page-break-inside: avoid;
    break-inside: avoid;
  }
  /* A printed page loses every link, so surface the destination inline. */
  /* Every internal link is root-relative, so the old http-only rule matched
     nothing on a printed page. */
  main a[href^="http"]::after { content: " (" attr(href) ")"; font-size: 9pt; }
  main a[href^="/"]::after { content: " (k9ids.com" attr(href) ")"; font-size: 9pt; }
  main a[href^="#"]::after, main a[href^="tel:"]::after { content: ""; }
  /* The blanket `background: transparent !important` above erases these
     pseudo-element markers, which are drawn purely with a background. */
  .checklist li::before, .eyebrow::before, .longform ul li::before { background: #000 !important; }
  .step__num { border: 1px solid #000 !important; }
  .faq-item[open] .faq-item__body, .faq-item .faq-item__body { display: block !important; }
  .js-reveal { opacity: 1 !important; transform: none !important; }
  @page { margin: 1.6cm; }

  /* -- Capability sheet: one page, no site chrome --
     The site footer is the only thing between this and a clean single sheet,
     and it is outside <main>, so nothing the page itself renders can hide it.
     Two independent rules, NOT one selector list: an unsupported :has() would
     invalidate the whole list and take the reliable selector down with it.
     Rule 1 is the one to rely on - the page sets $bodyClass = 'sheet-page'.
     Rule 2 is the safety net for the day someone forgets. */
  .sheet-page .site-footer { display: none !important; }
  body:has(.capability-sheet) .site-footer { display: none !important; }

  /* Fitting one page is mostly about type size, and every rule here has to
     outrank the generic print sizes above: those are element selectors (0,0,1)
     and these are (0,1,1), so they win wherever they apply and leave the rest
     of the site's printing untouched. */
  .capability-sheet__inner { max-width: none; padding: 0 !important; }
  .capability-sheet h1 { font-size: 17pt; }
  .capability-sheet h2 { font-size: 12pt; margin-top: 0.35cm; }
  .capability-sheet h3 { font-size: 9pt; }
  .capability-sheet p, .capability-sheet li { font-size: 9.5pt; line-height: 1.38; }
  .capability-sheet__grid { gap: 0.5cm 0.9cm; margin-top: 0.5cm; }
  /* A block split across a page break is exactly how a "one-page" document
     becomes two. */
  .capability-sheet__block { page-break-inside: avoid; break-inside: avoid; }
  /* The blanket `color: #000 !important` above does not touch border-color
     when a border names its own colour, so these would otherwise print navy
     and grey on a black-and-white document. */
  .capability-sheet__head { border-bottom: 2px solid #000 !important; padding-bottom: 0.25cm; }
  .capability-sheet__foot { border-top: 1px solid #000 !important; margin-top: 0.5cm; padding-top: 0.25cm; }
  /* The whole point of the sheet is that it can be handed over, so the URL
     annotations that help on every other page are noise here. */
  .capability-sheet a[href^="/"]::after,
  .capability-sheet a[href^="http"]::after { content: ""; }
  .spec-table { box-shadow: none !important; border: 1px solid #999 !important; page-break-inside: avoid; break-inside: avoid; }
  .spec-table th, .spec-table td { border-top: 1px solid #999 !important; }
  .spec-table tr:first-child th, .spec-table tr:first-child td { border-top: 0 !important; }
}

/* Honour the OS "reduce motion" setting: kill transforms/animations, but
   make sure reveal elements are still fully VISIBLE rather than stuck at
   opacity 0 (which is what a blanket animation-off would otherwise cause). */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
  }
  .js-reveal { opacity: 1 !important; transform: none !important; }
  .hero::after { animation: none; }
}
