/*
Theme Name: KMC Spark
Template: kmc-base-theme
Author: KMC Spark LLC
Description: kmcspark.com — KMC Spark's own agency site. Hybrid one-pager (JMD-style structure) per kmc-spark-handoff-for-myke (Aug 2026) — ported near-verbatim from the delivered kmcspark-mockup-bold-v2.html (kmc-spark-handoff-for-myke package, outside this repo). Header/footer/front-page markup is fully custom (not the shared kmc-base-theme template-parts) since this site's nav/section shape no longer matches what those assume; this stylesheet is intentionally self-contained rather than layered on top of kmc-base-main.css's component classes.
Version: 0.2.1
Text Domain: kmcspark
*/

/* Compiled from assets/scss/ by scripts/build-css.mjs. Do not edit this
   file directly -- run `npm run build:css` (or `watch:css`), and see that
   script for why the generated CSS is committed. */
/* ==========================================================================
   HOW THIS FILE IS ORGANISED -- read before adding a media query (2026-08-25)

   ONE BLOCK PER BREAKPOINT. Every width-based @media condition appears
   exactly once. Add a responsive rule to the existing block for its width;
   do not open a second one. This was consolidated on 2026-08-25 from 27
   blocks down to 19 -- max-width:900px alone had FIVE copies scattered over
   2,000 lines, plus two each of 440/480/720.

   WHY IT MATTERS, because this is not merely tidiness: three separate bugs
   in a single day were all the same failure -- a rule written in one block
   was silently overridden by an equal-specificity rule further down the
   file, so the edit looked like it simply had no effect. Duplicated blocks
   are what made that possible. Notable casualties: `.theme-toggle
   {display:none}` never hid anything, and a `.hero p.sub` font-size had to
   compete with four other copies of itself.

   THE CASCADE RULE. Media rules carry no extra specificity -- an @media
   rule beats a plain rule for the same selector ONLY by coming later in the
   file. So a `.foo` base rule placed AFTER a `@media` block wins at every
   width, which reads as "my media query is broken". If a media rule seems
   ignored, search the whole file for the selector before changing values.

   ORDER IS NOT WIDEST-FIRST, and that is a live footgun. The width blocks
   currently run 782, 360, 440, 520, 640, 420, 720, 900, 480. Because later
   wins, a narrower block placed BEFORE a wider one loses inside its own
   range -- e.g. the 720px block sits before the 900px block, so anything
   set in both resolves to the 900px value at <=720px. Nothing collides
   today (verified), but check both blocks before adding a rule that exists
   in two overlapping breakpoints.

   prefers-reduced-motion is DELIBERATELY still scattered (9 blocks). Those
   sit next to the animations they switch off so the guard is written in the
   same place as the thing it guards, they only ever set animation/
   transition to none, and none of them collide. Leave them colocated.

   If you move a block: moving one DOWN the file is safe (no base rule below
   any block shares a selector+property with it -- verified); moving one UP
   is not, and needs a before/after computed-style check.
   ========================================================================== */
:root {
  --accent-highlight: #B6E85C;
  --accent-core: #8CD211;
  --accent-deep: #5C8C0B;
  --accent-on: #0A0A0A;
  --accent-glow: rgba(140, 210, 17, 0.4);
  /* Text-only variant of --accent-core — same value everywhere except
     [data-theme="light"] below. --accent-core itself stays locked/identical
     in both modes (buttons, borders, glows, icons); this token exists so the
     handful of rules that use the lime as actual TEXT color can go through
     it instead, without touching every other --accent-core usage on the page. */
  --accent-core-text: var(--accent-core);
  /* Two real roles, replacing the single --font token this file used to have.
     That token's stack was 'General Sans','Inter',... — one family for the
     entire site, with Inter sitting behind it as a fallback that never
     rendered. So the "General Sans display + Inter body" pairing the brand
     docs describe was never true in the CSS; this is the first time it is.

     --font-display is Bitter, chosen as the closest free web face to the
     serif LOGO wordmark (see kmc_spark_enqueue_fonts() in functions.php).
     Headings, the nav wordmark and big stat numerals go through it; that is
     the whole point — the wordmark is itself a display setting, so display
     type is where matching it reads, and where a serif costs nothing in
     legibility.

     --font-body stays sans for everything a visitor actually reads or types
     into: paragraphs, form fields, buttons, nav labels. A slab serif at
     14-16px in a form input reads slower and heavier, and works against the
     "modern platform" positioning the copy is doing.

     Georgia is the display fallback rather than a bare `serif` because it is
     present on effectively every Windows/macOS/iOS/Android device and is
     metrically far closer to Bitter (large x-height, sturdy serifs) than the
     Times-ish default `serif` resolves to — so the pre-swap frame looks like
     the same design rather than a different one. */
  --font-display: 'Bitter', georgia, 'Times New Roman', serif;
  --font-body: 'Inter', -apple-system, blinkmacsystemfont, 'Segoe UI', sans-serif;
  /* ONE micro-label treatment, replacing four hand-rolled near-copies
     (2026-08-26, per request: "make sure all fonts are consistent").

     A computed-style audit of the live homepage found five separate
     uppercase/tracked label styles that all do the same job and none of
     which matched:

       .eyebrow-kicker      0.75rem   / 700 / 1.5px  (~0.125em)
       .footer-col-heading  0.6875rem / 800 / 0.12em
       .hero-brief-title    0.6875rem / 800 / 0.12em
       .work-card-industry  0.75rem   / 700 / 0.04em
       .work-card-badge     0.6875rem / 800 / 0.05em

     Two sizes, two weights and four trackings for what a reader perceives
     as one recurring element. The differences are all too small to read as
     deliberate and too large to be invisible -- which is precisely the
     "fonts look inconsistent" complaint, and why it was hard to point at.

     --label-* is the standalone form (section kickers, column headings,
     card meta). --pill-* is the badge form: the same face and weight at a
     step smaller and noticeably tighter, because a pill's own padding
     already does the separating that tracking does elsewhere, and 0.12em
     inside a 12px-tall pill reads as stretched rather than as considered.
     Those are the only two, and a new label should reuse one, not invent
     a sixth. */
  --label-size: 0.75rem;
  --label-weight: 800;
  --label-tracking: 0.12em;
  --pill-size: 0.6875rem;
  --pill-weight: 800;
  --pill-tracking: 0.07em;
  /* --eyebrow-tracking is deliberately a THIRD value, not a slot to fold
     into --label-* or --pill-*. It exists for exactly one element: the splash
     intro's "KMC SPARK" brand mark (#splashText .eyebrow), whose 0.22em
     tracking is intentionally wide -- tuned 2026-08-24 so its rendered
     width holds a fixed ~0.39x ratio against #splashHeadline's clamp() at
     every viewport, not a size anyone picked by eye. Pre-promotion audit
     UI-02 flagged this element as untokenised "12.4px/800/0.22em" -- naming
     it here is the fix: it's a real, deliberate third treatment, not the
     generic .eyebrow class drifting from --pill-*. If that clamp is ever
     retuned, this token and #splashHeadline's clamp move together. */
  --eyebrow-tracking: 0.22em;
}

/* Bold-direction green — locked, both modes. See docs/kmcspark/kmcspark-website-plan.md §0/§12. */
[data-theme=dark] {
  --bg: #0A0A0A;
  --bg-2: #111;
  --card-bg: #161816;
  --text: #F2F2F2;
  --text-secondary: #9AA0A0;
  --border: #232623;
  --nav-bg: #0A0A0A;
  --chip-border: #2A2E2A;
  /* Form fields need their OWN fill, not --card-bg (2026-08-26, per request:
     the fields look like "dangling text"). They were --card-bg sitting on a
     --card-bg panel: in light mode that is #FFF on #FFF, so a 1px #E1E6E2
     hairline was the only thing saying a field was there at all -- which is
     exactly why the captions read as text floating above nothing.
     A field is a WELL: one step away from the surface it sits on, in
     whichever direction that surface allows. Darker than the card here,
     lighter-tinted than the card in light mode. */
  --field-bg: #101210;
  --field-border: #2E332E;
  /* :root's --accent-deep (#5C8C0B) computes to 4.91:1 against --bg here
     but only 4.43:1 against --card-bg -- short of the 4.5:1 floor for the
     one eyebrow-kicker that sits on a card (.contact-book's "Book Your
     Call"), found in the same 2026-08-19 pre-launch audit as the light-
     mode --accent-deep fix above. A small nudge toward --accent-core
     (10% of the way, computed against the harder --card-bg case) clears
     both backgrounds with room to spare -- 4.83:1 on --card-bg, 5.36:1 on
     --bg -- without the bigger, visibly-brighter jump reusing the full
     --accent-core-text value would have been. */
  --accent-deep: #61930C;
}

[data-theme=light] {
  --bg: #F7FBEF;
  --bg-2: #FFF;
  --card-bg: #FFF;
  --text: #12160F;
  --text-secondary: #5B655D;
  --border: #E1E6E2;
  --nav-bg: #FFF;
  --chip-border: #D9E0D6;
  --field-bg: #F6FAF1;
  --field-border: #D5DED2;
  /* --accent-core (#8CD211) as text on this theme's --bg/--card-bg computes
     to ~1.7:1 — well under WCAG AA's 3:1 floor for large text, let alone the
     4.5:1 normal text needs (confirmed by Lighthouse, 2026-08-14 audit).
     This darker value on the same hue is ~6.2-6.5:1 against both --bg and
     --card-bg. --accent-core itself is untouched — locked per the brand
     decision above — this only redirects the text-color usages. */
  --accent-core-text: #466709;
  /* --accent-deep (#5C8C0B, used only by .eyebrow-kicker's small-caps label
     and the header scroll-progress bar's gradient) missed the fix above --
     found in a 2026-08-19 pre-launch audit computing every eyebrow-kicker
     sitewide at 3.84:1 in light mode, still short of the 4.5:1 small-text
     floor even though it's darker than raw --accent-core. Reuses
     --accent-core-text's already-vetted value here rather than a new,
     unverified shade -- the scroll-progress bar (the only non-text user)
     just gets a marginally darker gradient start, which is a 3px-tall
     decorative bar with no contrast requirement of its own. */
  --accent-deep: #466709;
}

/* All font-size values on this page are in rem (already confirmed — no px font-sizes
   anywhere in this file, 2026-08-17 audit) against this root, so they scale together
   and respect a visitor's own browser/OS text-size setting. Bumped 16px -> 18px
   (112.5%) 2026-08-17 per request to make all text larger; since every font-size in
   this file is rem-relative, this one change scales the whole page's type proportionally
   instead of hand-editing every individual rule. */
html {
  font-size: 112.5%;
}

/* Skip link -- WCAG 2.4.1 Bypass Blocks. Same pattern as the shared
   theme's .skip-link (_shared/kmc-base-theme/assets/css/main.css); this
   theme dequeues that stylesheet entirely (see functions.php) so it needs
   its own copy, themed off this file's own tokens. Hidden off-screen until
   keyboard focus lands on it -- header.php prints it as the first element
   after <body>, before the splash markup -- then jumps into view so a
   keyboard/AT user can skip the header (and, on the homepage, the splash
   overlay) and land on <main id="main">. */
.skip-link {
  position: absolute;
  top: -999px;
  left: 0;
  z-index: 10000;
  background: var(--card-bg);
  color: var(--text);
  border: 1px solid var(--border);
  padding: 12px 20px;
  font-family: var(--font-body);
  font-weight: 700;
  font-size: 0.95rem;
  border-radius: 0 0 10px 0;
}
.skip-link:focus-visible {
  top: 0;
  left: 0;
  outline: 3px solid var(--accent-core);
  outline-offset: -3px;
}

*, *::before, *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

/* Form controls do NOT inherit font-family: every browser substitutes its own
   UA default (Arial here), so any control that does not name a family lands
   off-brand. Measured on the live homepage before this rule existed -- four
   elements were rendering in Arial: .hamburger-btn, both .work-dot buttons
   and .sound-toggle. None of them show much text, which is exactly why it
   survived every visual pass; it was only visible by reading computed styles.

   `inherit` rather than `var(--font-body)` deliberately -- a control inside a
   heading or a display-face block should follow its own context, and this way
   there is one place that can never disagree with body/heading rules. The
   explicit families on .btn/.theme-toggle/.contact-form controls are now
   redundant but harmless, and are left where their own comments explain them.

   font-size is NOT reset here on purpose: the UA default matters for a few
   controls (the emoji in .sound-toggle among them) and every control this
   site styles already sets its own. */
button, input, select, textarea, optgroup {
  font-family: inherit;
}

body {
  font-family: var(--font-body);
  background: var(--bg);
  color: var(--text);
  overflow-x: hidden;
}

/* Soft-blend gutters (2026-08-27, per request -- "complementing backgrounds
   ... white gutters and keep same bg for middle"). Below 1380px .hero/
   .section's own gutter formula bottoms out at a flat 40px margin, so
   there's nothing worth distinguishing; at 1380px and up the gutter opens
   up and the capped column just floats in a page-coloured field with no
   edge of its own. This repaints body's own background (neither .hero nor
   .section paint their own, so this shows straight through) as one
   horizontal gradient: card-colour at the far edges, --bg through the
   whole content column, blending between the two over an 80px zone
   centred on the column's own edge -- not a hard cut, so there's no line
   to see, just a wide soft vignette that happens to be greenest in the
   middle.
   var(--card-bg) rather than a literal white so this adapts correctly in
   dark mode too: --card-bg is white in light theme (the literal ask) and
   the existing dark card colour in dark theme, never a jarring white flash
   against near-black content.
   The edge position is the exact same max(40px, calc(50% - 650px))
   formula .hero/.section use for their own margin, so the blend is always
   centred on the true column edge regardless of viewport width -- not a
   separately-tuned value that could drift out of sync with a future change
   to that formula. */
@media (min-width: 1380px) {
  body {
    background: linear-gradient(to right, var(--card-bg) 0%, var(--card-bg) calc(max(40px, 50% - 650px) - 80px), var(--bg) calc(max(40px, 50% - 650px) + 80px), var(--bg) calc(100% - max(40px, 50% - 650px) - 80px), var(--card-bg) calc(100% - max(40px, 50% - 650px) + 80px), var(--card-bg) 100%);
  }
}
/* No transition on body's background-color/color: confirmed live this
   causes the theme toggle to get stuck — the --bg/--text custom properties
   update correctly on click (verified directly via getComputedStyle), but
   the transitioned background-color/color properties that consume them via
   var() never settle to the new value, even seconds later. A transition
   whose animated property is driven by a custom property changing on an
   ancestor (here, [data-theme] on <html>, not a class on body itself) is a
   known browser edge case, not something specific to this markup — the
   safe fix is an instant switch instead of risking a half-switched page. */
@media (prefers-reduced-motion: reduce) {
  * {
    animation: none !important;
    transition: none !important;
  }
}
a {
  color: inherit;
  text-decoration: none;
}

/* ---------- Header / Nav ---------- */
/* Three-column grid, wordmark centred (2026-08-25, per request). Was
   flex + space-between with the wordmark bundled into .nav-left beside the
   hamburger.

   `1fr auto 1fr` IS THE POINT, and it is deliberately not a breakpoint.
   The two side tracks take an equal share of leftover space, so the middle
   track lands on the viewport's centre line -- but `1fr` is
   `minmax(auto, 1fr)`, so neither side can be squeezed below its own
   content. When the header runs out of room the wider side (the CTA) simply
   keeps its width and the wordmark slides off-centre. It degrades instead of
   overflowing, with no media query to maintain.

   That matters because a hard breakpoint would have been WRONG here.
   Measured: centring needs 2 x max(left, right) + wordmark + gaps, which at
   375px wide is 395px against 343px available -- so the single most common
   phone width cannot centre at all, while 320px (wordmark hidden, so the
   middle track is just the icon) and 430px+ fit fine. A `min-width` switch
   would have had to snap on and off around the busiest width on the site;
   `1fr auto 1fr` just tightens continuously.

   MEASURED ACROSS 17 WIDTHS (320-1600), so the two imperfect bands below
   are known and expected -- they are not a bug to re-fix. Offset of the
   wordmark from the viewport centre line, negative = sits left:
       320, 360 ............ 0px   (wordmark hidden, icon centres)
       375 -> 430 .......... -26px -> 0px
       441 -> 520 .......... -40px -> -1px
       560 and up .......... 0px
   Clearance to the CTA never drops below 14px and no width overflows.
   The jump at 441 is the theme toggle returning to the header (it lives in
   the nav tray at <=440), which widens the right track by ~47px. Closing
   that band would mean keeping the toggle in the tray up to ~520px --
   deliberately NOT done: removing a working control to square up a rare
   viewport band trades function for symmetry.

   Centring at 375px is not reachable without shrinking the CTA or the
   wordmark -- checked, including a smaller wordmark, which still leaves it
   short. Being a few px off-centre there is the better trade.

   .scroll-progress is a fourth child but is position:absolute, so it is out
   of grid flow and takes no track. */
header {
  position: sticky;
  top: 0;
  z-index: 220;
  /* FLEX, not the `1fr auto 1fr` grid this was between 2026-08-25 and
     2026-08-26. The wordmark was a direct child of <header> purely so it
     could sit in a centred middle track; it has moved back into .nav-left
     (per request), which leaves this bar with exactly two in-flow children
     -- brand on the left, CTA on the right. That is what space-between is
     for, and it retires three declarations the grid needed and flex does
     not: the centring tracks, `.nav-left { justify-self: start }` and
     `.nav-right { justify-self: end }`.

     .scroll-progress is the third child and is position: absolute, so it is
     out of flow here exactly as it was out of the grid.

     The gap survives as a real floor rather than decoration. Under the
     centred grid the surplus could all land on one side and put the
     wordmark flush against the CTA -- measured 0px between them at 375px.
     space-between makes that far less likely, but a bar whose two halves
     can touch at any width still wants a minimum. The 440px block below
     tightens it to 10px. */
  display: flex;
  justify-content: space-between;
  column-gap: 14px;
  align-items: center;
  padding: 20px 40px;
  background: var(--nav-bg);
  border-bottom: 1px solid var(--border);
}

/* 2026-08-19, per request ("doesn't look proper or professional" --
   screenshot showed the hamburger tray opened while logged in, admin
   toolbar visible above the header). WordPress's own #wpadminbar is
   position:fixed at the very top with a z-index (99999) nothing on this
   page can out-rank -- with top:0 above and no offset of its own, this
   header sinks underneath it the moment the page scrolls (top:0 has no
   idea the toolbar is there). Only a logged-in admin/editor viewing their
   own front end ever sees it -- never a real visitor -- but that's
   exactly who's looking at this constantly while managing content.
   Heights/breakpoint match WordPress core's own admin-bar.css exactly
   (32px above 782px, 46px at or below it), not this theme's own 900px
   breakpoint, since the toolbar's own height is what's being matched. */
body.admin-bar header {
  top: 32px;
}

@media screen and (max-width: 782px) {
  body.admin-bar header {
    top: 46px;
  }
}
/* Wraps the hamburger ONLY -- the wordmark moved out to be a direct grid
   child when the header was centred (2026-08-25). No `gap` here any more:
   with a single child it could never do anything, and a dead gap invites
   someone to "fix" the header spacing by tuning a value with no effect.
   Header spacing is the grid's column-gap now. */
/* The gap is back, and it is load-bearing again (2026-08-26). This wrapped
   only the hamburger while the wordmark was centred, so a gap on it could do
   nothing and was removed as a dead lever; the logo has moved back in beside
   the button, so it is once more the hamburger-to-wordmark distance. There is
   no `justify-self` any more -- <header> is flex, not a grid. */
.nav-left {
  display: flex;
  align-items: center;
  gap: 12px;
}

/* THE NAV LOGO IS REAL TEXT, SET IN THE SITE'S OWN DISPLAY FACE (2026-08-26,
   per request: "update the logo font to match the site font"). It was the
   lockup's own cropped artwork between 2026-08-25 and this date, and before
   that it was text; this is deliberately a return to text, not a regression.

   WEIGHT 400 IS MEASURED, NOT TASTE, AND IS THE ONE VALUE HERE MOST LIKELY TO
   BE "TIDIED" INTO SOMETHING HEAVIER. The complaint that started all of this
   was "it feels like the font is different but i'm not 100% sure", and the
   family was never the problem -- the navbar was already Bitter, the same
   face as every heading. The WEIGHT was: .logo had been swept into the
   `h1-h6, .logo` selector and so inherited headings' 700, while the real
   lockup is light. Measured both sides the same way -- narrowest run of the
   "K" stem between 15% and 85% of cap height, over cap height:
       real lockup   0.101
       Bitter 400    0.114   <- closest the family can get
       Bitter 700    0.206   <- what the navbar was rendering
   So 700 renders at roughly twice the stroke weight of the lockup that is
   still sitting in the footer of the same page. Anything above 400 walks back
   toward that. .logo is out of the `h1-h6` selector for good; do not add it
   back, and do not "match the headings" by raising this.

   WHAT TEXT CANNOT DO, stated plainly so it is a known cost rather than a
   later discovery: the real lockup carries a spark glyph between "KMC" and
   "Spark" that no font supplies, so this is a close approximation of the
   brand mark and not the mark itself. That trade was made knowingly --
   matching the site's type is being preferred over matching the lockup
   exactly. The full artwork lockup still appears in the footer.

   font-size is load-bearing: the 900px block steps it to 1.0625rem, which is
   what shrinks the logo on phones. */
.logo {
  display: inline-block;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: 1.25rem;
  letter-spacing: -0.01em;
  line-height: 1;
  color: var(--text);
  text-decoration: none;
}

/* THE 360px TIER IS GONE (2026-08-26), and it must not come back in its old
   form. It clipped .logo-name so the round icon could carry the header alone
   below 360px -- there is no icon in the header any more, so hiding the name
   would leave the brand link with no visible content and no accessible name
   at all. The logo IS the text now; it is the last thing in this bar that can
   be given up, not the first.

   That whole apparatus was sized for a bar this one no longer has. For the
   record, since the measurements were expensive: the header once held four
   items needing 357px (hamburger 44 + logo 139 + theme toggle 37 + CTA 115 +
   gaps 22), which fit only at 390px and up and pushed 53px of horizontal
   scroll at 320px. The theme toggle moved into the tray on 2026-08-26
   (~52px back) and the icon left the lockup on the same day, so the bar is
   now three things: a 40px button, a text wordmark, and the CTA.

   The lever when it stops fitting is .logo's font-size -- shrink the name,
   never hide it -- and that is exactly what the rule below does. Verify by
   measurement at 320 / 360 / 375, which is how every number here was arrived
   at. */
/* 320px needs this and 375px does not. MEASURED: at 320 the bar came to 4.8px
   more than its own padding box, which flex absorbed by squeezing the right
   gutter to 11.2px against the left's 16px -- nothing clipped and the document
   never scrolled, so it read as "slightly off" rather than as a bug, which is
   the same way the 440px gutter problem presented.

   0.9375rem takes the wordmark 96px -> 84.8px and puts the gutters back level
   at 16/16 with ~6px to spare. WATCH THE UNIT: root font-size is 18px at this
   width, not 16 -- the theme scales root type on mobile -- so this is ~16.9px,
   and a rem value read as 16-based here will be wrong by an eighth. 1rem was
   tried first and left only 0.8px of slack, which fits but is too thin a
   margin for a name that comes from an ACF field and could get longer.

   `header .logo`, NOT `.logo`, and the extra selector is load-bearing. The
   900px block ~4000 lines below sets `.logo { font-size: 1.0625rem }`; a bare
   `.logo` here ties it on specificity and then LOSES on source order, so this
   rule would be silently inert while looking completely correct. This file
   records the same trap twice already (.theme-toggle, and .nav-cta's padding,
   which needed three classes for the same reason). */
@media (max-width: 360px) {
  header .logo {
    font-size: 0.9375rem;
  }
}
/* The round fingerprint icon that used to sit beside the wordmark here was
   removed from the navbar on 2026-08-26, per request, along with the
   on-demand spin it carried and that spin's prefers-reduced-motion guard.

   THE ASSET IS STILL IN USE ELSEWHERE -- kmcspark-navicon-lime-80.png is the
   logo in the Auditor's outbound follow-up email (inc/auditor.php). Do not
   delete the file as an unreferenced leftover.

   @keyframes spin-slow went with it: this was its only consumer. */
/* .nav-wordmark and .logo-name are gone (2026-08-26). The wordmark was two
   dark/light <img>s inside a wrapper span; the logo is a single text node
   now, so there is nothing to swap per theme and nothing to wrap. Colour
   comes from .logo's own `color: var(--text)`, which follows the theme by
   itself. kmcspark-wordmark-{lime,black}.png are left on disk unreferenced,
   like the other retired brand exports in that directory. */
/* No justify-self any more: <header> is a two-item flex bar with
   space-between, so this is pinned right by the layout itself rather than by
   a grid alignment property. */
.nav-right {
  display: flex;
  align-items: center;
  gap: 14px;
}

/* Sized to MATCH .hamburger-btn's 40px exactly, so the bar's two controls are
   the same height (2026-08-26). .btn's own 11px padding plus its 2px border
   came to 45px, which made the CTA the tallest thing in the bar and the brand
   the shortest. 9px + 2px border + a 1.2 line-height on 0.875rem type lands
   on 40. (They no longer bracket the logo -- the logo moved back to the left,
   beside the hamburger -- but the height match is still what stops the bar
   looking stepped.) */
.nav-cta {
  height: 40px;
  padding: 0 20px;
}

/* The narrow-width gutter fix. What used to sit here also shrank the round
   logo icon to claw back width; that icon left the header on 2026-08-26, so
   only the gutter half survives.

   PLACED AFTER .nav-cta's OWN BASE RULE ON PURPOSE, and this is the part
   worth keeping. Both are single-class selectors, so a cascade tie resolves
   by source order -- an earlier version of this block sat ~45 lines above the
   base rule and was silently inert, while the header still measured 1px over
   and the media query looked like it had simply not matched. This file
   records the identical trap for .theme-toggle. Check source order before
   concluding a narrow-width override is not applying. */
@media (max-width: 440px) {
  /* The header's own right gutter. Under the old three-track grid the tracks
     plus their gaps came to 354.3px inside a 343px content box, so the grid
     overflowed its padding and `justify-self: end` parked the CTA at the
     OVERFLOWED edge: 4.7px from the viewport against the hamburger's 16px on
     the left. Nothing clipped and nothing scrolled, so it read as "slightly
     off" rather than as a bug. 6px back from the button's padding and 8px
     from the gap -> inside the box with room.

     The bar is flex now and far less likely to overflow at all, but these
     two values are still what keeps the right gutter matched to the left one
     at phone widths, so they stay.

     THREE class selectors on the padding rule, and every one is needed. The
     900px block much further down this file sets `.nav-right .btn-primary
     { padding: 9px 16px }`. A bare `.nav-cta` loses to it on specificity
     (1 class vs 2) and `.nav-right .nav-cta` TIES it and then loses on source
     order, because that block sits later. Both were tried and both were
     silently inert -- as .nav-cta's own base `padding: 0 20px` had already
     been, which is how this was found at all. */
  header {
    column-gap: 10px;
  }
  .nav-right .btn-primary.nav-cta {
    padding: 0 14px;
  }
}
/* Pill (icon + always-visible text), not the old icon-only circle -- a
   title/aria-label tooltip explaining "click for Dark" etc. is no help at
   all on a touch device, which can't hover, and reported live that the bare
   glyph wasn't obvious even on desktop. Text content itself (Light/Dark/
   Auto) is written by theme-toggle.js, matching its title/aria-label logic
   exactly so what's visible and what a screen reader announces never
   disagree. */
.theme-toggle {
  height: 38px;
  padding: 0 14px 0 12px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: transparent;
  color: var(--text);
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 7px;
  font-family: var(--font-body);
  font-size: 0.8125rem;
  font-weight: 700;
}

.theme-toggle-icon {
  font-size: 1rem;
  line-height: 1;
}

.theme-toggle-label {
  white-space: nowrap;
}

.btn {
  font-family: var(--font-body);
  font-weight: 700;
  font-size: 0.875rem;
  padding: 11px 22px;
  border-radius: 8px;
  cursor: pointer;
  border: 2px solid transparent;
  line-height: 1.2;
  /* Buttons on this site are routinely STRETCHED wider than their own label
     -- .cta-banner-action and .hero-ctas are both flex columns with
     align-items: stretch. Without this the label sits at the start edge of
     whatever width it was given; see the note on .btn:has(.btn-icon) below,
     where the flex-container case needs justify-content for the same reason. */
  text-align: center;
  transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.25s ease;
}
.btn:hover {
  transform: translateY(-3px) scale(1.03);
}
.btn:active {
  transform: translateY(-1px) scale(0.97);
}

.btn-primary {
  background: var(--accent-core);
  color: var(--accent-on);
}
.btn-primary:hover {
  box-shadow: 0 0 24px var(--accent-glow);
}

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

/* Icon + label buttons (2026-08-26, per request: "add visual polish/icon to
   the book a call button").

   inline-flex on the button, not just a margin on the svg: the label is a
   real <span> beside the icon, and floating the icon with a margin leaves
   the two on different baselines the moment the label wraps. `align-items:
   center` on a nowrap row is what keeps the glyph optically centred against
   a cap-height word rather than sitting on the text baseline.

   .btn-icon carries no colour of its own -- the SVG strokes currentColor,
   so it follows the button through light, dark, the lime .btn-primary fill
   and the inverted fill inside .cta-banner without a single extra rule.

   The slight lift on hover is the same 2px the whole button already moves,
   applied again to the icon only, so the glyph leads the motion instead of
   riding it. Costs nothing; reads as intent. */
/* justify-content: center is LOAD-BEARING, not tidiness (2026-08-26, per
   request: "make sure all of these buttons text are properly aligned").
   .cta-banner-action is a flex column with align-items: stretch, so the
   button inside it fills the column -- 401px wide on the homepage. A flex
   button wider than its own content leaves that content wherever
   justify-content puts it, and the default is the START edge: measured 28px
   of space on the left and 126.6px on the right, a 98.6px skew, on the site's
   most important button. text-align cannot fix this one, because the icon and
   the label are flex ITEMS, not text -- which is why .btn carries both. */
.btn:has(.btn-icon), .nav-cta {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 9px;
}

.btn-icon {
  flex: none;
  transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}

.btn:hover .btn-icon {
  transform: translateY(-1px) scale(1.06);
}

/* Explicit keyboard focus ring. The slider dots and .spark-card already
   define one; buttons were relying on the UA default, which is nearly
   invisible sitting on the lime --accent-core fill. Offset so it reads
   outside the button rather than against its own background. */
.btn:focus-visible {
  outline: 3px solid var(--text);
  outline-offset: 3px;
}

/* --accent-on, not var(--bg): the banner button is now near-black in BOTH
   themes (see .cta-banner .btn-primary), and var(--bg) resolves to that
   same near-black in dark mode -- an invisible focus ring on the site's
   most important button. A dark ring on the lime panel reads in both. */
.cta-banner .btn:focus-visible {
  outline-color: var(--accent-on);
}

.industry-hero .btn:focus-visible {
  outline-color: var(--bg);
}

/* ---------- Hamburger (paired with logo, top-left) ---------- */
.hamburger-wrap {
  position: relative;
  display: flex;
  align-items: center;
}

/* 40px, not 44 (2026-08-26, per request: "navbar elements dont look
   proportional"). MEASURED before changing anything, which is what made the
   complaint concrete: the bar held a 44px circle, a 30px logo icon beside a
   21.7px wordmark, and a 45px CTA. The brand lockup was the SMALLEST thing in
   its own header and the wordmark was smaller than the icon next to it, while
   a 19px bolt sat inside 44px of ring.

   Now 40 / 30+25 / 40. The two controls match each other exactly, the logo
   sits between them, and the bolt grew to 20px so the circle reads as a
   button rather than as a ring with something lost inside it. */
.hamburger-btn {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  /* THE RING IS THE BOLT'S OWN COLOUR, so the button reads as one designed
     object rather than a glyph sitting inside an unrelated circle
     (2026-08-26, chosen from a rendered five-option comparison).

     IT IS ALSO AN ACCESSIBILITY FIX, which is the part that must survive any
     future restyle. The previous `1px solid var(--border)` measured 1.26:1
     against the light nav (#E1E6E2 on #FFF) and 1.29:1 against the dark one
     (#232623 on #0A0A0A) -- under WCAG 1.4.11's 3:1 floor for the visual
     boundary of a control, and on screen the ring was essentially not there
     at all. --accent-core-text measures 6.55:1 and 10.69:1. Whatever this
     becomes later, keep it above 3:1 in BOTH themes.

     --accent-core-text, NOT raw --accent-core, and the distinction is the
     same one .hamburger-bolt's note below already records: the locked brand
     lime computes to ~1.7:1 on the light theme's surfaces, so light
     substitutes a darker green (#466709) on the same hue. The raw token
     would fail light mode while looking perfectly correct in dark.

     KNOWN AND ACCEPTED, recorded so it is not "discovered" later as a bug:
     in DARK mode --accent-core-text resolves to full lime, the same colour
     as .nav-cta's fill, so the bar carries two lime objects and the ring has
     some pull against the button opposite it. That was raised before this
     option was chosen, and chosen anyway. If the CTA ever needs to win the
     bar back, the neutral runner-up was 2px var(--text-secondary) at
     6.06:1 / 7.46:1.

     box-sizing is border-box globally, so 1px -> 2px keeps this at exactly
     40px and the height match with .nav-cta holds. Measured: zero layout
     shift, 20px bolt inside a 36px inner box. Do not add width to
     compensate. */
  border: 2px solid var(--accent-core-text);
  background: transparent;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
  z-index: 210;
  flex-shrink: 0;
}

/* .hamburger-label (a hover/focus-reveal "Menu" text) removed 2026-08-19,
   per request -- see header.php's docblock at the hamburger button for why:
   no accessibility benefit over the button's own aria-label, and a
   confusing "stuck" artifact on real touch devices. */
/* Spark-bolt menu icon (kmcspark-mockup-bold-v2_19.html): a lightning-bolt
   glyph tying back to the KMC Spark mark, crossfading into an X when the
   tray is open -- replaces the earlier generic 3-bar hamburger. */
/* 20px, up from the SVG's own 19 (2026-08-26) -- see .hamburger-btn's note.
   The circle came down 4px and the glyph went up 1, which is what stops it
   reading as a small mark lost inside a large ring. */
.hamburger-icon {
  width: 20px;
  height: 20px;
  position: absolute;
  transition: opacity 0.28s ease, transform 0.32s cubic-bezier(0.16, 1, 0.3, 1), filter 0.25s ease;
}

.hamburger-bolt {
  /* --accent-core-text, not raw --accent-core (2026-08-19 audit finding):
     the locked brand lime only computes to ~1.7:1 against light mode's
     --bg/--card-bg -- under WCAG 1.4.11's 3:1 floor for a graphical object
     that identifies a control, same underlying issue the light-mode
     --accent-core-text/--accent-deep tokens above were already introduced
     to fix for TEXT. This is the one remaining raw --accent-core usage
     that's actually an icon rather than a decorative fill. */
  color: var(--accent-core-text);
  opacity: 1;
  transform: rotate(0deg) scale(1);
  filter: drop-shadow(0 0 0 transparent);
}

.hamburger-x {
  color: var(--text);
  opacity: 0;
  transform: rotate(-90deg) scale(0.5);
}

.hamburger-btn:hover .hamburger-bolt {
  filter: drop-shadow(0 0 7px var(--accent-glow));
  transform: scale(1.12);
}

.hamburger-btn.open .hamburger-bolt {
  opacity: 0;
  transform: rotate(90deg) scale(0.5);
}

.hamburger-btn.open .hamburger-x {
  opacity: 1;
  transform: rotate(0deg) scale(1);
}

.nav-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.6);
  z-index: 190;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.3s ease;
}
.nav-overlay.open {
  opacity: 1;
  pointer-events: auto;
}

/* top/height are driven by --header-h (main.js measures the real header
   box and keeps this in sync on resize) rather than a hardcoded offset --
   2026-08-19, per request ("missing space between navbar and hamburger
   menu start"). The old flat padding-top:100px was a guess tuned against
   one header height and drifted anywhere the header's own height differs
   (this site's hamburger nav is shared by every breakpoint, and mobile's
   header{padding:14px 16px} vs desktop's 20px 40px don't render the same
   height) -- deriving it from the real box means the tray starts flush
   under the header on every breakpoint with no separate tuning, and the
   fallback (73px) only ever matters for the one frame before JS runs. */
.nav-tray {
  position: fixed;
  top: var(--header-h, 73px);
  left: 0;
  height: calc(100% - var(--header-h, 73px));
  width: min(380px, 86vw);
  background: var(--bg-2);
  border-right: 1px solid var(--border);
  z-index: 200;
  transform: translateX(-100%);
  transition: transform 0.35s cubic-bezier(0.16, 1, 0.3, 1);
  padding: 28px 40px 40px;
  display: flex;
  flex-direction: column;
  gap: 4px;
  overflow-y: auto;
}
.nav-tray.open {
  transform: translateX(0);
}

/* Font-size/padding both cut down 2026-08-19, per request ("decrease the
   font and padding") -- was 1.625rem/14px, sized for a 7-8pt drop shadow
   the mockup never actually needed at this length of list. 1.1875rem still
   clears 4.5:1 body-text minimum comfortably at this weight, and 11px
   padding still gives each row a ~44px tap target (WCAG 2.5.5) since the
   link is a full-width block, not a shrink-to-fit target. */
.nav-tray a {
  font-size: 1.1875rem;
  font-weight: 700;
  padding: 11px 0;
  border-bottom: 1px solid var(--border);
  opacity: 0;
  transform: translateY(12px);
  transition: opacity 0.35s ease, transform 0.35s ease, color 0.2s ease;
  display: block;
}

.nav-tray.open a {
  opacity: 1;
  transform: translateY(0);
}

/* nth-of-TYPE, not nth-child (2026-08-26): .nav-tray-tools is now the
   tray's first child, so nth-child(1) no longer matches the first LINK.
   Counting <a> elements only makes the stagger independent of anything
   non-link that gets added to this tray. */
.nav-tray.open a:nth-of-type(1) {
  transition-delay: 0.06s;
}

.nav-tray.open a:nth-of-type(2) {
  transition-delay: 0.11s;
}

.nav-tray.open a:nth-of-type(3) {
  transition-delay: 0.16s;
}

.nav-tray.open a:nth-of-type(4) {
  transition-delay: 0.21s;
}

.nav-tray.open a:nth-of-type(5) {
  transition-delay: 0.26s;
}

.nav-tray.open a:nth-of-type(6) {
  transition-delay: 0.31s;
}

.nav-tray.open a:nth-of-type(7) {
  transition-delay: 0.36s;
}

/* 8/9 added 2026-08-27 when Service Areas joined the tray -- with Insights
   present (page_for_posts published + >=1 post) the tray now holds 9 links,
   one more than these rules covered before, so Contact (and About, when
   Insights is showing) would have animated in with no delay instead of
   finishing the wave. */
.nav-tray.open a:nth-of-type(8) {
  transition-delay: 0.41s;
}

.nav-tray.open a:nth-of-type(9) {
  transition-delay: 0.46s;
}

.nav-tray a:hover {
  color: var(--accent-core-text);
}

/* Explicit ring, matching the site's other focus-visible treatments
   (.spark-insights-card h3 a, etc.) -- added 2026-08-19 alongside main.js
   now focusing the first link on open, so that focus move is actually
   visible instead of relying on each browser's own default outline. */
.nav-tray a:focus-visible {
  outline: 2px solid var(--accent-core);
  outline-offset: -2px;
  border-radius: 4px;
}

/* The tray is focused programmatically on open (see main.js) and is a
   container rather than a control, so it takes no ring of its own -- the
   standard exception, and the reason the link rule above stays intact.
   :focus, not :focus-visible: the container must never draw a ring by
   either route, and it is not reachable by Tab (tabindex="-1"). */
.nav-tray:focus {
  outline: none;
}

/* The tray toolbar (2026-08-26). This is now the ONLY theme control on the
   site -- header.php's copy is gone, and with it the 440px breakpoint that
   used to swap the two. What was here before: `.nav-tray-theme { display:
   none }` plus a @media (max-width: 440px) block that hid the header button
   and revealed this one, with a comment on each explaining which rule had to
   stay after which. Both are deleted rather than neutralised; there is one
   button now, so there is nothing left to sequence.

   Laid out as a labelled row ("Appearance" + pill) with its own bottom rule,
   which is what separates a SETTING at the top of the tray from the list of
   DESTINATIONS under it. Without the rule the pill just reads as an odd
   first menu item. */
.nav-tray-tools {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 0 0 16px;
  margin-bottom: 8px;
  border-bottom: 1px solid var(--border);
}

.nav-tray-tools-label {
  font-size: var(--pill-size);
  font-weight: var(--label-weight);
  letter-spacing: var(--label-tracking);
  text-transform: uppercase;
  color: var(--text-secondary);
}

/* Sized and spaced like the header button it replaces rather than like the
   nav rows below it -- it is a control, not a destination, so it reads as a
   pill instead of as a full-width row with a rule under it. */
.nav-tray-theme {
  flex: none;
  height: 34px;
  padding: 0 13px 0 11px;
  gap: 7px;
  border-radius: 999px;
  font-size: 0.75rem;
}

/* The header hid this label for space. There is room here, and the word is
   what makes the three-state light/dark/auto cycle legible on a touch device
   that cannot hover for the tooltip. */
.nav-tray-theme .theme-toggle-label {
  display: inline;
}

.nav-tray-theme:hover {
  border-color: var(--accent-core);
  color: var(--accent-core-text);
}

/* ---------- Hero ---------- */
/* Gutter via margin (max(40px, calc(50% - 650px)), same formula as
   .cta-banner/footer below) instead of max-width:1300px + margin:0 auto --
   the two techniques only agree once the viewport is >=1380px or <=1300px;
   in between (an ordinary laptop/browser-window width), max-width's auto
   margin hasn't reached 40px yet, so a max-width+auto-margin section sits
   measurably closer to the viewport edge than a margin-formula element
   right next to it. Same fix applied to .section/.back-link-row/
   .industry-hero below for a consistent content width everywhere. */
/* overflow:hidden and isolation:isolate were removed 2026-08-23 along with
   the decorative hero glow they existed for -- the first clipped that
   pseudo-element's 70px blur radius, the second gave it its own stacking
   context. Nothing else in .hero is absolutely positioned, so neither had
   any other job. position:relative is kept: harmless, and the obvious thing
   to want if anything is ever positioned against the hero box. */
.hero {
  padding: 60px 0 90px;
  margin: 0 max(40px, 50% - 650px);
  position: relative;
}

/* No max-width here (was 680px, then 960px): that cap was sized for the old
   two-column hero (text + spinning graphic mark). Now that the hero is
   text-only, any cap here leaves dead space on the right at desktop widths,
   unlike every other section on the page -- .hero p.sub below is also
   uncapped now, per the same reasoning, so the whole hero column (headline,
   paragraph, CTA row) uses the full section width, matching
   .timeline/.spark-grid/.accordion/etc. */
.hero-content {
  max-width: none;
  position: relative;
  z-index: 1;
}

/* The decorative hero background glow (added 2026-08-22 for "a better visual
   first impression" -- a blurred, aurora-drifting radial gradient behind the
   text) was REMOVED 2026-08-23 per request: it simply didn't look good. Do
   not reinstate it without asking; this was a judgement call on the visual,
   not a performance decision, so "it's only CSS and runs on the compositor"
   is not a counter-argument to it.

   It was, incidentally, also the page's one permanently-running animation --
   an 18s infinite alternate on a filter:blur(70px) layer, i.e. real
   compositing work for the entire time anyone had the homepage open, not
   just during an intro. The aurora-drift @keyframes itself is still used by
   .spark-card-aurora and .industry-hero-media::before, so it stays. */
.eyebrow {
  display: inline-block;
  background: var(--accent-core);
  color: var(--accent-on);
  font-weight: var(--pill-weight);
  font-size: var(--pill-size);
  letter-spacing: var(--pill-tracking);
  padding: 6px 14px;
  border-radius: 999px;
  margin-bottom: 18px;
}

/* Section kicker — every non-hero section used the hero's own solid pill
   badge (just recolored via an inline style, repeated 7x in front-page.php),
   the identical rounded-badge-before-every-heading pattern that reads as a
   generic SaaS template. A short tick + label is the same "eyebrow" job
   without the copy-pasted pill shape. */
.eyebrow-kicker {
  display: flex;
  align-items: center;
  gap: 10px;
  font-weight: var(--label-weight);
  font-size: var(--label-size);
  letter-spacing: var(--label-tracking);
  color: var(--accent-deep);
  margin-bottom: 16px;
}
.eyebrow-kicker::before {
  content: "";
  width: 24px;
  height: 2px;
  background: var(--accent-core);
  flex-shrink: 0;
}

/* ---------- Display type (the logo-matching serif) ----------
   Every heading, plus the one non-heading thing that's doing a display job:
   the nav wordmark (.logo — it renders the literal words "KMC Spark" right
   next to the icon, so it is the one piece of live text on the page that
   sits beside the real wordmark and gets compared to it). The About stat
   numerals (.about-stat strong) used to be a second exception here; that
   markup was removed 2026-08-22 (see front-page.php's docblock at the old
   #about stat-row spot), so this selector list dropped it too rather than
   carry a dead selector.

   Deliberately NOT included: .eyebrow / .eyebrow-kicker and .accordion-num.
   Small uppercase letter-spaced text is the one place a slab serif reliably
   falls apart — the serifs collide with the tracking — and the brand doc's
   own type scale already lists the eyebrow under the body face, not display.

   .footer-col-heading is excluded for the SAME reason despite being an <h2>
   tag (2026-08-19 audit fix) — it's uppercase, 0.12em-tracked, 11px, and its
   own comment above calls it out as "the same eyebrow treatment the sections
   use." Being an h2 got it caught by this selector anyway on the first pass;
   visually it was the worst offender of this whole swap — a tracked
   all-caps serif label sitting directly next to a plain-Inter "COMING SOON"
   pill in the footer's 4th column, which is exactly the collision this
   paragraph already warned about.

   The weight reset here is load-bearing, not cosmetic. The old sans ran
   headings at 900 with tight negative tracking, which is how you give a
   geometric sans presence. Bitter at 900 is a wall of black slabs, and the
   actual logo wordmark is LIGHT — so carrying 900 over would have matched the
   family while missing the thing being matched. 700 with tracking relaxed to
   normal is what reads as the same voice as the lockup. Individual rules
   below still set their own sizes; only weight and tracking are normalized,
   and anything that genuinely wants to be heavier can override after this.

   .logo IS NOT IN THIS SELECTOR and must not be added back. The paragraph
   above reasons about matching the lockup and then applies a HEADING weight,
   which is the right call for h1-h6 and the wrong one for the wordmark
   itself: 700 measured at twice the lockup's stroke weight. The nav logo is
   real text again as of 2026-08-26 and sets its own 400 -- so it is once
   more exactly the kind of thing this selector would silently capture. See
   .logo for the measurements. */
h1, h2, h3, h4, h5, h6 {
  font-family: var(--font-display);
  font-weight: 700;
  letter-spacing: normal;
}

/* .footer-col-heading is an <h2> but is content-wise an eyebrow (see the
   comment above and that rule's own docblock) — send it back to the body
   face rather than trying to exclude it from the selector list above, since
   its own rule already sets the uppercase/tracking/size that make it read
   as a label, not a heading. */
.footer-col-heading {
  font-family: var(--font-body);
}

h1 {
  font-size: 3.25rem;
  line-height: 1.08;
  letter-spacing: -0.5px;
  margin-bottom: 20px;
}
h1 .accent {
  color: var(--accent-core-text);
}

.hero p.sub {
  color: var(--text-secondary);
  font-size: 1rem;
  line-height: 1.6;
  margin-bottom: 28px;
}

.hero-ctas {
  display: flex;
  gap: 14px;
  margin-bottom: 34px;
  flex-wrap: wrap;
}

/* Reassurance line under the primary CTA (2026-08-18) -- a full-width flex
   row inside .hero-ctas rather than a sibling after it, so the container's
   own margin-bottom stays the single spacing source of truth (it's shared
   with .industry-hero, which has no note). Negative margin-top pulls it
   tight under the button, cancelling the container's 14px gap. */
.hero-cta-note {
  flex-basis: 100%;
  margin-top: -4px;
  font-size: 0.8125rem;
  color: var(--text-secondary);
}

/* Visually-hidden but screen-reader-available. WordPress's own convention
   name, but this theme dequeues kmc-base-main entirely (see
   kmc_spark_dequeue_shared_theme_assets() in functions.php), so nothing
   defines it for us -- without this rule the label it's applied to renders
   as ordinary visible text. Used for form labels whose purpose is already
   obvious sighted (a placeholder alone leaves a control unlabelled). */
.screen-reader-text {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* ---------- Audit entry (inside #audit's row) ---------- */
/* v3 (2026-08-18, per request — "combine these together to the same row"):
   was .hero-audit, a callout card sitting in the hero, duplicating the
   #audit section directly below it. Both merged into one .audit-row inside
   #audit; these rules kept their shape (bordered card, same input treatment
   as .contact-form so the page never gains a third input style) and only
   moved house, so the visual language is unchanged. */
/* v3 (2026-08-18, per request — "the audit CTA still doesn't flow well"):
   the copy-left/input-right grid is gone. It made #audit the only section
   whose header sat in a side column rather than running full width, so it
   read as a detour from the page's top-to-bottom rhythm. The header now
   stacks like every other section and this is just the form beneath it.
   Bounded width is kept deliberately: a full-bleed 1300px text input would
   be unusable, and the request was that HEADERS and DESCRIPTIONS run full
   width — a form control isn't either. Row layout (input grows, button
   sits beside it) rather than the old stacked column, since as a standalone
   block under the copy it reads as one "enter URL -> go" control instead of
   two tall full-width fields. */
/* The payoff, above the ask (2026-08-25) -- see front-page.php's #audit for
   why this section needed one at all. */
.audit-teaser-lede {
  /* 60ch, matching .why-us-lede's measured-text discipline rather than
     running the full section width. */
  max-width: 60ch;
  margin: -12px 0 20px;
  font-size: 0.9375rem;
  line-height: 1.6;
  color: var(--text-secondary);
}

/* The five checks. Chips rather than a list: this is a scan-in-one-second
   glance at what the audit covers, not something to read line by line, and a
   wrapping row reads as a set at any width without a media query.

   NOT interactive, and styled so it does not pretend to be -- no pointer
   cursor, no hover state, and a deliberately flatter background than
   .filter-tab (which IS a control and lives in the same visual family).
   Chips that look clickable and are not is the single easiest way to make a
   section feel broken. */
.audit-check-chips {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 0 0 24px;
  padding: 0;
  max-width: 640px;
}
.audit-check-chips li {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 7px 13px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--card-bg);
  font-size: 0.75rem;
  font-weight: 700;
  color: var(--text-secondary);
  line-height: 1;
}

/* A tick, as a pseudo-element: decorative, so it must not reach a screen
   reader -- the label beside it already carries the meaning, and five
   announced check marks would be noise. --accent-core-text rather than raw
   --accent-core for the 3:1 non-text contrast floor in light mode, same
   reasoning as .included-card li::before. */
.audit-check-chips li::before {
  content: "✓";
  color: var(--accent-core-text);
  font-weight: 800;
  font-size: 0.8125rem;
}

/* Phone widths: pack tighter. At the desktop size these wrapped to FOUR rows
   at 375px (147px measured), which was most of what this section gained.

   PADDING AND GAP ONLY -- the type stays at 0.75rem. A first attempt also
   dropped it to 0.6875rem (12.375px), which bought one more row back but put
   these chips BELOW the smallest type anywhere else on the page
   (.audit-entry-note, 13.5px) -- and this is the copy telling a stranger what
   they get for an email address, so it is the wrong place on the site to own
   the smallest text. Three rows at a readable size beats two at a squint.

   Its own media query, colocated with the rules it modifies, rather than
   appended to one of the file's existing 480/720px blocks -- there are
   already duplicate blocks at both, which is exactly how an earlier edit in
   this pass got silently overridden. */
@media (max-width: 520px) {
  .audit-check-chips {
    gap: 6px;
    margin-bottom: 20px;
  }
  .audit-check-chips li {
    padding: 6px 10px;
    gap: 5px;
  }
  .audit-teaser-lede {
    margin-bottom: 16px;
  }
}
.audit-entry {
  max-width: 560px;
}

.audit-entry-form {
  display: flex;
  gap: 10px;
  align-items: stretch;
}
.audit-entry-form input {
  flex: 1 1 auto;
  min-width: 0;
  font-family: var(--font-body);
  font-size: 0.875rem;
  padding: 12px 14px;
  border-radius: 8px;
  border: 1px solid var(--border);
  background: var(--card-bg);
  color: var(--text);
  transition: border-color 0.25s ease, box-shadow 0.3s ease;
}
.audit-entry-form input:focus {
  outline: none;
  border-color: var(--accent-core);
  box-shadow: 0 0 0 4px var(--accent-glow);
}
.audit-entry-form .btn {
  flex: 0 0 auto;
  justify-content: center;
  white-space: nowrap;
}

.audit-entry-note {
  margin-top: 10px;
  font-size: 0.75rem;
  color: var(--text-secondary);
  line-height: 1.5;
}

/* @keyframes spin-slow removed 2026-08-26 -- its only consumer was the nav
   logo icon, which is no longer in the header. */
/* ---------- Sections generic ---------- */
.section {
  padding: 70px 0;
  margin: 0 max(40px, 50% - 650px);
}

/* Headings run the full container width (2026-08-18, per request). They
   were capped at 640px, which forced a hard wrap into the middle of nearly
   every section heading and left orphans on the second line -- "See what
   your website is costing / you.", "See the difference a real platform /
   makes.". At this size 640px only fits ~38 characters, and most of these
   headings are 30-45. Uncapped, almost all of them set on one line, and the
   ones that don't (About's, the longest on the page) break at a natural
   point instead of a forced one. Body copy stays measured -- .section-lead
   below is still 560px -- so this widens the heading only, which is the
   line that should be able to breathe. */
.section h1, .section h2 {
  font-size: 2.125rem;
  font-weight: 700;
  margin-bottom: 30px;
}

/* Full width, matching the headings above them (2026-08-18, per request:
   "make sure the headers and sub heads/description are also consistently
   full width"). Was capped at 560px, which put every section's description
   on a different, narrower measure than its own heading — a second ragged
   edge inside every section. These leads are short (one or two sentences),
   so uncapped most of them set on a single line directly under a
   single-line heading, which is the point: one left edge, one right edge,
   one rhythm. Longer body copy that genuinely needs a reading measure keeps
   its own — see .why-us-lede at 640px. (.about-grid-single used to be a
   second example here too; went full width 2026-08-20, per request --
   see that rule's own docblock.) */
.section-lead {
  font-size: 0.9375rem;
  color: var(--text-secondary);
  margin-top: -14px;
  margin-bottom: 30px;
  line-height: 1.6;
}

/* ---------- Free Site Audit ---------- */
/* Multi-step reveal (url input -> UI-only "scanning" beat, no network call
   -> generic locked results preview -> lead form -> done) -- lives on its
   own page now (page-free-audit.php, /free-audit/, 2026-08-18 per request),
   see that file's docblock for why this isn't a real scan. front-page.php's
   own .audit-teaser section reuses this same class only for its lightweight
   homepage teaser card, not the full flow. The two forms inside reuse
   .contact-form/.form-row/.form-submit-error verbatim rather than a second
   set of input styles. */
/* The intro column above the audit card, normalised (2026-08-26, per
   request: "this doesn't look right"). Measured at 1280px before the fix,
   one column of five stacked blocks had FOUR different right edges --
   h1 and .section-lead at 1225, .audit-trust and the AI callout at 600,
   .audit-card at 680 -- and four type sizes that went 38.25, 16.875, 14.625,
   15.75, i.e. shrinking twice and then growing again.

   .audit-trust is on 640px, which is .audit-card's own max-width, so the
   trust line shares one left and one right edge with the tool it introduces.
   The h1 and .section-lead deliberately stay full width: they are this
   section's header, and every other section on the site sets them that way
   (see .section-lead's own docblock). Two groups with two measures is a
   structure; four measures was a pile.

   The .audit-ai-callout half of that pair was removed later the same day --
   see page-free-audit.php for why. The measure it shared with .audit-trust is
   the surviving half of this fix, so do not widen the trust line back to full
   width now that it stands alone; it still introduces a 640px card. */
/* Cold-traffic context line above the audit card (page-free-audit.php) --
   quieter than .section-lead, since it's orientation for a stranger rather
   than part of the pitch. */
.audit-trust {
  font-size: 0.875rem;
  color: var(--text-secondary);
  line-height: 1.6;
  max-width: 640px;
  margin: -18px 0 26px;
}

/* .audit-ai-callout was here and is gone (2026-08-26) along with its markup.
   It was the last rule on the site using the accent-left-border "thesis
   statement" treatment, so that language is now retired rather than merely
   unused -- .why-us-lede gave it up earlier the same day. Anything reaching
   for a left-accent panel from here on is reintroducing a treatment, not
   reusing one. */
.audit-card {
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 16px;
  padding: 34px;
  max-width: 640px;
}

.audit-step {
  display: none;
}

form.audit-step.is-active {
  display: flex;
}

div.audit-step.is-active {
  display: block;
}

.audit-step.is-active {
  animation: sparkCardIn 0.4s cubic-bezier(0.16, 1, 0.3, 1) both;
}

@media (prefers-reduced-motion: reduce) {
  .audit-step.is-active {
    animation: none;
  }
}
/* .audit-scan-spinner / -label / -checks removed 2026-08-25 with the fake
   scan step they styled (see page-free-audit.php). The pending state is now
   the submit button itself. */
.audit-done-message {
  font-size: 0.9375rem;
  font-weight: 700;
  color: var(--text);
}
.audit-done-message strong {
  color: var(--accent-core-text);
}

/* Optional booking on the audit done step (see page-free-audit.php's
   #auditBooking docblock). Deliberately quieter than #contactBooking on the
   homepage: there, booking is the goal of the section, so it gets a card and a
   lock panel. Here the goal is already met -- the lead is saved and the audit
   is on its way -- so this reads as an offer, not a next required step. Hence
   the hairline top rule instead of a card, and no locked state at all: it
   simply is not in the DOM until the submit succeeds. */
.audit-book {
  margin-top: 26px;
  padding-top: 22px;
  border-top: 1px solid var(--chip-border);
  text-align: left;
}

.audit-book-lead {
  font-size: 1rem;
  font-weight: 700;
  color: var(--text);
  margin: 0 0 6px;
}

.audit-book-note {
  font-size: 0.875rem;
  color: var(--muted);
  margin: 0 0 16px;
  max-width: 52ch;
}

.audit-book-frame {
  display: flex;
  min-height: 260px;
}

/* The iframe keeps its own [hidden] until revealAuditBooking() gives it a src,
   so it stays out of the tab order and the a11y tree while it has nothing to
   show -- an empty iframe is still focusable otherwise. */
.audit-book-iframe {
  flex: 1;
  width: 100%;
  height: 600px;
  border: 0;
  border-radius: 10px;
}

@media (max-width: 640px) {
  .audit-book-iframe {
    height: 520px;
  }
}
/* "You run the business, we run the growth" line in #why-us (2026-08-18).
   Heavier than .section-lead and accent-bordered, because it's the section's
   actual thesis rather than a subtitle -- the cards below are the evidence
   for it. */
/* Identical to .section-lead as of 2026-08-26, per request: "lets get rid of
   the green line and match the sub headers of other homepage sections like
   free site audit that have the grey text etc keep it consistent."

   It used to be a thesis-statement treatment -- 1rem, full --text, a 640px
   measure and a 3px accent left border -- and .hero-sub's own rule below still
   carries the argument for it ("that line is the page's thesis, the cards below
   are its evidence"). That argument is now overruled: on the homepage this
   paragraph sits in exactly the slot every other section fills with a plain
   grey .section-lead, one scroll below the audit section's own, and a reader
   moving down the page reads the difference as two kinds of thing rather than
   as emphasis.

   The class is kept rather than swapped for .section-lead in the markup,
   because both front-page.php and page-why-us.php use it and .hero-sub
   .why-us-lede below still needs a handle for the subpage measure.

   The accent-left-border language was kept alive at the time by
   .audit-ai-callout, on the grounds that a genuine callout beside its text is
   a different thing from a lead paragraph under a heading. That rule was
   removed later the same day, so the treatment is now gone from the site
   entirely and this is simply how a lede is set here. */
.why-us-lede {
  font-size: 0.9375rem;
  line-height: 1.6;
  color: var(--text-secondary);
  margin: -14px 0 30px;
}

/* ---------- Why Us contrast strip (homepage #why-us) ---------- */
/* #why-us teaser cards (2026-08-26, per request -- an earlier pass made the
   wording parallel and left the FORMAT alone, which is why it did not read as
   a change at all: "I don't see the why us changes").

   These are now the card every other homepage section uses: a heading and one
   prose sentence, the same shape as .spark-insights-card and as the /why-us/
   page's own .why-us-card.

   WHAT THIS REPLACED WAS A DELIBERATE DECISION, not an accident -- a two-line
   "Instead of / You get" comparison, added 2026-08-25, whose own note argued
   that four concrete contrasts earned the space better than cards did. That
   is superseded on request, with the tradeoff understood and stated: the
   side-by-side comparison is gone, and the section now argues in prose the
   way the rest of the page does. Restoring it means restoring the markup in
   front-page.php too; the two are one component.

   The heading is a real <h3>. It was a <p> styled to look like one, so this
   section contributed nothing to the document outline while every other card
   on the page did -- and /why-us/ already used <h3> for the identical card,
   so the two pages disagreed with each other as well.

   The prose is industry_tagline: content that ALREADY EXISTED and had been
   rendering nowhere on this page since the circle grid that carried it was
   removed. /why-us/ still renders it, so teaser and full page now say the
   same sentence instead of two hand-maintained variants that drift apart. */
.why-us-teaser-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 18px 24px;
  margin: 0 0 4px;
  max-width: 900px;
}

/* Border + shadow + a 4px lift is .work-card's exact combination, carried over
   from the rows this replaced -- not a fifth hover variant. The global
   prefers-reduced-motion rule at the top of this file already kills the
   transition, so no per-rule guard is needed. */
.why-us-teaser-card {
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--card-bg);
  padding: 18px 20px;
  transition: border-color 0.3s ease, box-shadow 0.3s ease, transform 0.3s ease;
}
.why-us-teaser-card:hover {
  border-color: var(--accent-core);
  box-shadow: 0 18px 44px -18px rgba(0, 0, 0, 0.45);
  transform: translateY(-4px);
}

/* Display face at 600 -- the settled card-rank convention, 700 being reserved
   for h1-h2.

   ONE SELECTOR, deliberately. A first version scoped this as
   `.section .why-us-teaser-card h3` as well, guarding against the trap that
   has twice shipped a card heading at full section size here. That guard
   defends against nothing: the rule in question is `.section h1, .section h2`
   and there is no `.section h3` anywhere in this file. What it DID do was
   raise specificity to (0,2,1) and silently beat the responsive
   `margin-bottom` override in the max-width block below -- measured, 8px
   where 7px was declared. .spark-insights-card h3 is a lone selector for the
   same reason; a card heading that out-specifies its own media query is a
   worse bug than the one being guarded against.

   1.0625rem is the house card-title size, not a new value -- measured off
   .service-name and .accordion-title strong on this same page, both 19.125px
   Bitter 600. The <p> this replaced was 0.9375rem, which made it the only
   card-rank title on the homepage set smaller than its peers; keeping that
   size would have preserved the exact inconsistency this pass exists to fix. */
.why-us-teaser-card h3 {
  font-family: var(--font-display);
  font-size: 1.0625rem;
  font-weight: 600;
  line-height: 1.35;
  margin: 0 0 8px;
  color: var(--text);
}

.why-us-teaser-card p {
  font-size: 0.8125rem;
  line-height: 1.6;
  margin: 0;
  color: var(--text-secondary);
}

/* Client testimonial (2026-08-18, audit finding C-02). Sits between the
   Our Work heading and the case-study slider, so the claim and its
   corroboration read together. Renders only when a real quote + attribution
   exist (see front-page.php) -- there is intentionally no placeholder state,
   because a fabricated testimonial is worse than none. Deliberately quiet:
   an accent rule and larger type rather than a card, so it reads as
   testimony rather than another piece of marketing furniture. */
.client-quote {
  margin: 0 0 34px;
  padding-left: 22px;
  border-left: 3px solid var(--accent-core);
  max-width: 780px;
}
.client-quote blockquote {
  margin: 0 0 12px;
  font-size: 1.125rem;
  line-height: 1.55;
  font-weight: 600;
  color: var(--text);
  letter-spacing: -0.01em;
}
.client-quote blockquote::before {
  content: "“";
}
.client-quote blockquote::after {
  content: "”";
}
.client-quote figcaption {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 10px;
}

.client-quote-name {
  font-size: 0.8125rem;
  font-weight: 800;
  color: var(--text);
}

.client-quote-role {
  font-size: 0.8125rem;
  color: var(--text-secondary);
}

/* Interim Google Business Profile link (CONV-03). Deliberately plain text,
   not a badge/card -- it's a stopgap pointing at real third-party proof,
   not a piece of marketing furniture claiming a rating of its own. */
.gbp-review-link {
  margin: -20px 0 34px;
  font-size: 0.875rem;
}
.gbp-review-link a {
  color: var(--accent-core-text);
  font-weight: 700;
  text-decoration: none;
  border-bottom: 1px solid currentcolor;
}
.gbp-review-link a:hover {
  opacity: 0.8;
}

/* ---------- Why Us (differentiator checklist cards) ----------
   v2 (2026-08-17, per request): replaced the circular flip-card "bubbles"
   (hover/tap to reveal a blurb, link to a dedicated subpage) entirely — the
   4 subpages they linked to are gone too (trashed, see
   kmc_spark_retire_differentiator_subpages_v1() in functions.php). Same
   underlying industries CPT/ACF data, just rendered as a static, always-
   readable checklist card per differentiator instead of an interactive
   reveal gating the content behind a hover or a click-through. */
.diff-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 24px;
  margin-top: 10px;
}

.diff-card {
  border: 1px solid var(--border);
  background: var(--card-bg);
  border-radius: 16px;
  padding: 26px;
  transition: border-color 0.3s ease, box-shadow 0.3s ease;
}
.diff-card:hover {
  border-color: var(--accent-core);
  box-shadow: 0 14px 34px -18px rgba(0, 0, 0, 0.4);
}
.diff-card h3 {
  font-size: 1.125rem;
  font-weight: 600;
  margin-bottom: 8px;
}
.diff-card > p {
  font-size: 0.8125rem;
  color: var(--text-secondary);
  line-height: 1.6;
  margin-bottom: 16px;
}
.diff-card ul {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.diff-card li {
  font-size: 0.8125rem;
  color: var(--text);
  padding-left: 22px;
  position: relative;
  line-height: 1.5;
}
.diff-card li::before {
  content: "✓";
  position: absolute;
  left: 0;
  top: 0;
  color: var(--accent-core-text);
  font-weight: 800;
}

/* Dots for the mobile-only carousel (see the max-width:900px block below) --
   hidden by default since the desktop grid doesn't scroll/slide; same
   .work-dot/.spark-dot shape and behavior, own class so it isn't coupled to
   either. */
.diff-dots {
  display: none;
  justify-content: center;
  align-items: center;
  gap: 1px;
  margin-top: 10px;
}

/* 24x24 hit area, 8px painted dot -- see .work-dot for the reasoning. */
.diff-dot {
  width: 24px;
  height: 24px;
  padding: 0;
  border: none;
  background: none;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}
.diff-dot::before {
  content: "";
  width: 8px;
  height: 8px;
  border-radius: 999px;
  background: var(--chip-border);
  transition: background 0.25s ease, transform 0.25s ease;
}
.diff-dot.is-current::before {
  background: var(--accent-core);
  transform: scale(1.35);
}
.diff-dot:focus-visible {
  outline: none;
}
.diff-dot:focus-visible::before {
  outline: 2px solid var(--accent-core);
  outline-offset: 3px;
}

.industries-note {
  margin-top: 30px;
  text-align: center;
  font-size: 0.8125rem;
  color: var(--text-secondary);
}

/* ---------- Portfolio ("Our Work") — real client case studies ----------
   v2 (2026-08-17, per request): smaller cards in a native horizontal
   snap-scroll slider (dots + IntersectionObserver "current" tracking, see
   main.js's "'Our Work' portfolio slider" block) instead of a full-width
   auto-fit grid — same interaction language as the pricing section's mobile
   carousel, just active at every breakpoint since this grid never needed a
   separate desktop layout. Scales cleanly as more case studies get added:
   today it's just JMD + the trailing "add your site" CTA card. */
/* Desktop alignment (2026-08-18, layout audit): these three lines used to
   read scroll-padding-inline/padding: max(40px, calc(50% - 650px)) and
   margin: 10px min(-40px, calc(650px - 50%)) 0 — copied from .section's own
   `margin: 0 max(40px, calc(50% - 650px))`. That works on .section because
   its containing block is the viewport, so the 50% is viewport-relative.
   Here the containing block is .section itself (max 1300px), so 50% never
   exceeds 650px, calc() is never negative, and min() collapsed to a
   constant -40px at every width. Net effect: the slider sat exactly 40px
   proud of the grid on each side — not aligned to the page, not a real
   full-bleed either, just visibly wider than every neighbouring section.
   Now aligned to the container, with 12px of inline margin/padding that
   cancel out: content lines up with the headings above, while cards keep
   room to paint their hover shadow instead of having it clipped at the
   scroller edge. Mobile still bleeds edge-to-edge via the max-width:900px
   override far below, which is the right pattern there and measured clean
   (375px wide on a 375px viewport, no document overflow). */
.work-grid {
  display: flex;
  gap: 20px;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-padding-inline: 12px;
  margin: 10px -12px 0;
  padding: 4px 12px 10px;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  /* Grab cursor is the only hint a desktop visitor gets that this scrolls
     by dragging — the scrollbar is deliberately hidden above, and there's
     no touch input to discover it by. Matching main.js's enableDragScroll(),
     added 2026-08-18. Snap is switched off mid-drag so the browser isn't
     fighting the manual scrollLeft writes on every mousemove; it re-engages
     the moment .is-dragging comes off and the released scroll settles. */
  cursor: grab;
}
.work-grid::-webkit-scrollbar {
  display: none;
}

/* ---------- Horizontal-scroll affordance (2026-08-18, per request:
   "users know to slide/touch the bleeding edge cards on mobile ... right now
   it's just cut off content").

   Two separate problems were reported as one:

   1. .work-grid / .spark-insights-grid used scroll-snap-align:start with a
      12px gutter and a clamp(240px,80vw,300px) card, so on a 375px phone
      exactly ~31px of the next card peeked. A 31px sliver reads as a broken
      / clipped card, not as "there is more to the right". .diff-grid had
      already solved this properly at this same breakpoint (82% card, 9vw
      gutter both sides, centred snap, non-current cards dimmed) -- the fix
      below is that proven pattern applied to the other two, not a new one.
   2. Scrollbars are hidden on all three (scrollbar-width:none), so once the
      peek fails there is no affordance left at all.

   The mask is deliberately on the scroll container itself rather than a
   wrapper: a mask applies in the element's own padding-box coordinate
   space, so it stays pinned to the visible edges instead of scrolling away
   with the content -- no extra markup needed. Classes are set by main.js's
   enableScrollAffordance(); with JS off nothing is masked, which is the
   correct fallback (a permanently faded edge would be worse than none). */
.work-grid, .spark-insights-grid, .diff-grid {
  --edge-fade: 44px;
}

.work-grid.is-scrollable:not(.is-at-end),
.spark-insights-grid.is-scrollable:not(.is-at-end),
.diff-grid.is-scrollable:not(.is-at-end) {
  -webkit-mask-image: linear-gradient(to right, #000 calc(100% - var(--edge-fade)), transparent 100%);
  mask-image: linear-gradient(to right, #000 calc(100% - var(--edge-fade)), transparent 100%);
}

.work-grid.is-scrollable:not(.is-at-start),
.spark-insights-grid.is-scrollable:not(.is-at-start),
.diff-grid.is-scrollable:not(.is-at-start) {
  -webkit-mask-image: linear-gradient(to right, transparent 0, #000 var(--edge-fade));
  mask-image: linear-gradient(to right, transparent 0, #000 var(--edge-fade));
}

/* Mid-scroll: both edges. Listed last so it wins over the two single-edge
   rules above, which it shares specificity with. */
.work-grid.is-scrollable:not(.is-at-start):not(.is-at-end),
.spark-insights-grid.is-scrollable:not(.is-at-start):not(.is-at-end),
.diff-grid.is-scrollable:not(.is-at-start):not(.is-at-end) {
  -webkit-mask-image: linear-gradient(to right, transparent 0, #000 var(--edge-fade), #000 calc(100% - var(--edge-fade)), transparent 100%);
  mask-image: linear-gradient(to right, transparent 0, #000 var(--edge-fade), #000 calc(100% - var(--edge-fade)), transparent 100%);
}

.work-grid.is-dragging {
  scroll-snap-type: none;
  cursor: grabbing;
  user-select: none;
}

.work-card {
  /* flex: 1 0 — grow, never shrink (2026-08-18, layout audit). Was
     `0 0`, a hard-fixed 300px. That's right for a slider with lots of
     slides, but "Our Work" currently holds one real case study plus the
     trailing CTA card, so two 300px cards sat in a 1324px track and left
     704px — 53% of the row — visibly empty, with nothing to scroll to.
     grow:1 lets them expand into free space and fill the row; shrink:0 is
     what keeps the carousel a carousel — once enough cards exist that
     their bases overflow the track there's no free space to claim, so
     they hold 300px and scroll instead of squashing. max-width stops two
     cards stretching to half the page each. Self-adjusting as case
     studies get added: fills at 2-3, scrolls from ~5. */
  flex: 1 0 clamp(240px, 80vw, 300px);
  max-width: 660px;
  scroll-snap-align: start;
  border: 1px solid var(--border);
  background: var(--card-bg);
  border-radius: 16px;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  transition: border-color 0.3s ease, box-shadow 0.3s ease, transform 0.3s ease;
}
.work-card:hover, .work-card.is-current {
  border-color: var(--accent-core);
  box-shadow: 0 18px 44px -18px rgba(0, 0, 0, 0.45);
}
.work-card:hover {
  transform: translateY(-4px);
}

.work-card-badge {
  display: inline-block;
  background: var(--accent-core);
  color: var(--accent-on);
  font-size: var(--pill-size);
  font-weight: var(--pill-weight);
  letter-spacing: var(--pill-tracking);
  text-transform: uppercase;
  padding: 5px 12px;
  border-radius: 999px;
}

.work-card-industry {
  font-size: var(--label-size);
  font-weight: var(--label-weight);
  color: var(--text-secondary);
  text-transform: uppercase;
  letter-spacing: var(--label-tracking);
}

.work-card h3 {
  font-size: 1.125rem;
  font-weight: 600;
  margin-top: 2px;
}

/* Clamped to 4 lines — a full case-study excerpt (portfolio_url's `excerpt`,
   often a long paragraph) would otherwise stretch this card far taller than
   its content needs, and since .work-grid stretches every slide to match
   the tallest card, that dragged the short CTA card's height up with it,
   leaving it mostly empty (2026-08-17, per request — "not proportional"). */
.work-card p {
  font-size: 0.8125rem;
  color: var(--text-secondary);
  line-height: 1.6;
  display: -webkit-box;
  -webkit-line-clamp: 4;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* margin-top: auto (not the previous fixed 4px) is what actually pins this
   to the card's bottom edge -- it eats all of .work-card-body's now-flex:1
   free space above the link, so every card's "Read More" lands at the same
   height regardless of title/excerpt line count. */
.work-card-link {
  font-size: 0.8125rem;
  font-weight: 800;
  color: var(--accent-core-text);
  text-decoration: underline;
  margin-top: auto;
}

/* Trailing "add your site" CTA slide — same .work-card shape/size, dashed
   accent border instead of a photo so it reads as an invitation, not a
   fourth case study. */
.work-card-cta {
  text-decoration: none;
  color: inherit;
  cursor: pointer;
  justify-content: center;
  gap: 10px;
  padding: 24px 22px;
  border-style: dashed;
  border-color: var(--accent-core);
  background: var(--accent-glow);
}
.work-card-cta:hover, .work-card-cta.is-current {
  border-style: solid;
  box-shadow: 0 18px 44px var(--accent-glow);
}
.work-card-cta:hover {
  transform: translateY(-4px);
}
.work-card-cta h3 {
  font-size: 1.0625rem;
  font-weight: 600;
}

/* --text-secondary/--accent-core-text (this rule and .work-card-link's own
   override below) rather than the values those tokens carry elsewhere --
   both are tuned for contrast against the plain page --bg, not against
   this card's own --accent-glow wash. Found in a 2026-08-19 pre-launch
   audit: --text-secondary on this background computed to 2.96:1 in dark
   mode (needs 4.5), and green link text on a green-tinted card was a
   losing combination regardless of theme (4.24:1, green-on-green has
   little luminance difference to work with). --text passes comfortably
   in both themes; the card keeps its accent-green presence from the
   dashed border and the icon badge, so this isn't losing the brand color
   entirely, just moving it off text that has to stay readable. */
.work-card-cta p {
  font-size: 0.8125rem;
  color: var(--text);
  line-height: 1.55;
}

.work-card-cta .work-card-link {
  color: var(--text);
}

/* Override .work-card-link's margin-top: auto for the CTA slide specifically.
   That rule's job is pinning "Read More" to the bottom of a real card's
   body — flex items pull auto margins first, ahead of a container's own
   justify-content, so left alone it would also yank this CTA's link down to
   the very bottom of the card and break the centered icon/heading/copy/link
   group above (this card's content isn't wrapped in .work-card-body at all,
   it's direct children of .work-card-cta's own justify-content: center flex
   column, and gap: 10px already spaces them evenly). Caught in review before
   shipping, 2026-08-18 — never actually rendered wrong in production. */
/* stylelint-disable-next-line no-duplicate-selectors */
.work-card-cta .work-card-link {
  margin-top: 0;
}

.work-dots {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 1px;
  margin-top: 10px;
}

/* The painted dot stays 8px, but the BUTTON is 24x24 (2026-08-18): these
   were 8px hit targets, under WCAG 2.2 SC 2.5.8's 24px minimum and
   genuinely hard to tap on the exact control meant to prove the row
   scrolls. Sizing the pseudo-element instead of the button keeps the
   visual design identical. Gap shrinks to compensate for the new padding
   so the row's overall width is unchanged. */
.work-dot {
  width: 24px;
  height: 24px;
  padding: 0;
  border: none;
  background: none;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}
.work-dot::before {
  content: "";
  width: 8px;
  height: 8px;
  border-radius: 999px;
  background: var(--chip-border);
  transition: background 0.25s ease, transform 0.25s ease;
}
.work-dot.is-current::before {
  background: var(--accent-core);
  transform: scale(1.35);
}
.work-dot:focus-visible {
  outline: none;
}
.work-dot:focus-visible::before {
  outline: 2px solid var(--accent-core);
  outline-offset: 3px;
}

/* ---------- Insights (blog preview) — same snap-scroll slider as Our Work
   above (2026-08-17, per request), independent rules rather than a shared
   abstraction since the two aren't guaranteed to stay identical (e.g. dates
   here, industry badges there).
   v2 (2026-08-17, bugfix): renamed .insight(s)-* -> .spark-insights-* --
   home.php's own, unrelated blog-index template already owns .insights-grid/
   .insight-card/.insight-card-meta/.insight-card-title (see ~line 1519,
   "Insights (blog) -- index cards + single article") for its static CSS
   grid of post cards. Same class names, later source order, so that rule
   won the cascade and silently flattened this slider into a stacked grid
   on the homepage (no scroll-snap, no fixed card width) -- found live,
   "just looks like cards stacked" report. */
.spark-insights-grid {
  display: flex;
  gap: 20px;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  /* Same container-alignment fix as .work-grid — see its comment above for
     why the old percentage-based bleed formula collapsed to a constant. */
  scroll-padding-inline: 12px;
  margin: 10px -12px 0;
  padding: 4px 12px 10px;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  /* Same drag-to-scroll cursor affordance as .work-grid above, added
     2026-08-18 — see that rule's comment for why. Duplicated rather than
     shared, matching this file's own stated policy of not guaranteeing
     .spark-insights-* stays identical to .work-* over time. */
  cursor: grab;
}
.spark-insights-grid::-webkit-scrollbar {
  display: none;
}
.spark-insights-grid.is-dragging {
  scroll-snap-type: none;
  cursor: grabbing;
  user-select: none;
}

.spark-insights-card {
  /* Same grow-don't-shrink fix as .work-card — see its comment for the
     reasoning. Insights ran 3 cards in a 1324px track, leaving 384px
     empty; it fills now, and still scrolls once enough posts exist. */
  flex: 1 0 clamp(240px, 80vw, 300px);
  max-width: 660px;
  scroll-snap-align: start;
  border: 1px solid var(--border);
  background: var(--card-bg);
  border-radius: 16px;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  transition: border-color 0.3s ease, box-shadow 0.3s ease, transform 0.3s ease;
}
.spark-insights-card:hover, .spark-insights-card.is-current {
  border-color: var(--accent-core);
  box-shadow: 0 18px 44px -18px rgba(0, 0, 0, 0.45);
}
.spark-insights-card:hover {
  transform: translateY(-4px);
}

/* object-fit: CONTAIN, not cover — 2026-09-06, reported live from a phone.
   The featured images on these posts are 1200x630 link-preview cards (the
   same OG spec assets/images/kmcspark-og-image.png is built to), so cover was
   pouring a 1.90:1 graphic into this 1.60:1 box: it filled the height and
   discarded ~16% of the width, ~8% off each side, clipping the text baked
   into the artwork at both edges — "ASE STUDY" on the left, "kmcspark.cor"
   on the right. contain fits the whole image whatever ratio it arrives at,
   so no future upload can clip either; --bg-2 letterboxes the remainder,
   invisibly in light mode (--bg-2 and --card-bg are both #FFF there) and as
   a subtle inset well in dark.
   THE HOVER scale(1.04) THAT USED TO SIT ON THIS IMG IS GONE DELIBERATELY
   and must not come back: on a contained image it overflows the box
   horizontally by 4%, which re-clips 2% off each edge — this exact bug,
   reintroduced on hover, on the desktop half of the traffic. The card's own
   lift, border-colour and shadow already carry the hover state (see
   .spark-insights-card above); the transition on the img went with it
   because nothing animates that property here any more. */
.spark-insights-card-media {
  aspect-ratio: 16/10;
  overflow: hidden;
  background: var(--bg-2);
}
.spark-insights-card-media img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
}

/* Branded stand-in for a post with no featured image — 2026-09-06, shipped
   alongside the contain fix above. Before this the media div rendered only
   inside front-page.php's has_post_thumbnail() guard, so an imageless post
   produced a card with no media band at all and sat visibly shorter than its
   neighbours in a carousel whose entire job is a row of matching slides.
   DRAWN, not fitted: this is an element sized to the box, not an <img>
   object-fit'ed into it, so the label on it cannot clip the way baked-in text
   on a 1200x630 OG card does. That is the point of the whole approach — a
   thumbnail may carry text only when the text is drawn to the frame.
   Two selectors, not the bare modifier, because this overrides
   .spark-insights-card-media's own single-class `background` and a rule whose
   correctness rests on source order is invisible to whoever edits next — same
   trap the .insight-card-title / .diff-card h3 comments in _industries.scss
   record.
   The two deep greens are literals ON PURPOSE and are not a missed token:
   this is artwork standing in for a photograph, so it must render identically
   in both themes, unlike every chrome token around it. The lime over it is
   --accent-core/--accent-highlight, which _base.scss locks identical across
   modes and which per-client brand injection can replace, so a future
   client's palette recolours the panel for free. */
.spark-insights-card-media.spark-insights-card-media-placeholder {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  background: repeating-linear-gradient(58deg, rgba(255, 255, 255, 0.05) 0 2px, rgba(255, 255, 255, 0) 2px 22px), radial-gradient(72% 92% at 88% 6%, color-mix(in srgb, var(--accent-core) 58%, transparent) 0%, color-mix(in srgb, var(--accent-core) 0%, transparent) 62%), linear-gradient(150deg, #1E2A12 0%, #141D0B 100%);
  /* Percentage width so the mark tracks the card from its 300px mobile
     floor to its 660px max — kmc_spark_bolt_svg()'s width/height attributes
     are a fallback, not the intended size. */
}
.spark-insights-card-media.spark-insights-card-media-placeholder .kmc-bolt {
  width: 28%;
  height: auto;
  color: var(--accent-highlight);
}

.spark-insights-card-media-tag {
  position: absolute;
  left: 16px;
  bottom: 14px;
  font-size: var(--label-size);
  font-weight: var(--label-weight);
  letter-spacing: var(--label-tracking);
  text-transform: uppercase;
  line-height: 1;
  color: var(--accent-on);
  background: var(--accent-core);
  padding: 6px 11px;
  border-radius: 999px;
}

/* Same fix as .work-card-body — flex: 1 so the body claims the extra height
   the outer card's flex-stretch gives a shorter-content sibling, otherwise
   left unclaimed inside the card's own bottom padding. See that rule's
   comment for the full reasoning; confirmed live here too, 2026-08-18. */
.spark-insights-card-body {
  padding: 20px 22px 24px;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 8px;
  flex: 1;
}

/* No markup renders this today: front-page.php stopped printing the card
   date while every post shares one seeded publication date, so a row of
   identical stamps read as a fault. Homepage only -- /insights/ and the
   article still show theirs. Kept, not deleted, so the card reads
   correctly the day dates mean something here too. */
.spark-insights-card-date {
  font-size: var(--label-size);
  font-weight: var(--label-weight);
  color: var(--text-secondary);
  text-transform: uppercase;
  letter-spacing: var(--label-tracking);
}

.spark-insights-card h3 {
  font-size: 1.125rem;
  font-weight: 600;
  margin-top: 2px;
}

/* The headline is now the card's keyword-bearing link (see front-page.php's
   own note). It should read as a headline, not a link, until hovered --
   inherit rather than pick up the global anchor color. */
.spark-insights-card h3 a {
  color: inherit;
  text-decoration: none;
}
.spark-insights-card h3 a:hover {
  color: var(--accent-core-text);
}
.spark-insights-card h3 a:focus-visible {
  outline: 2px solid var(--accent-core);
  outline-offset: 3px;
  border-radius: 4px;
}

.spark-insights-card-body p {
  font-size: 0.8125rem;
  color: var(--text-secondary);
  line-height: 1.6;
  display: -webkit-box;
  -webkit-line-clamp: 4;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* margin-top: auto pins this to the card's bottom edge — see .work-card-link's
   comment for the mechanism. */
.spark-insights-card-link {
  font-size: 0.8125rem;
  font-weight: 800;
  color: var(--accent-core-text);
  text-decoration: underline;
  margin-top: auto;
}

/* Trailing "see all insights" CTA slide — same .spark-insights-card shape/
   size, dashed accent border instead of a photo, same pattern as
   .work-card-cta. */
.spark-insights-card-cta {
  text-decoration: none;
  color: inherit;
  cursor: pointer;
  justify-content: center;
  gap: 10px;
  padding: 24px 22px;
  border-style: dashed;
  border-color: var(--accent-core);
  background: var(--accent-glow);
}
.spark-insights-card-cta:hover, .spark-insights-card-cta.is-current {
  border-style: solid;
  box-shadow: 0 18px 44px var(--accent-glow);
}
.spark-insights-card-cta:hover {
  transform: translateY(-4px);
}

.spark-insights-card-cta-icon {
  width: 38px;
  height: 38px;
  border-radius: 999px;
  background: var(--accent-core);
  color: var(--accent-on);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.375rem;
  font-weight: 700;
  line-height: 1;
}

.spark-insights-card-cta h3 {
  font-size: 1.0625rem;
  font-weight: 600;
}

/* --text, not --text-secondary/--accent-core-text -- same contrast fix as
   .work-card-cta's identical rule, same reasoning: see that rule's comment. */
.spark-insights-card-cta p {
  font-size: 0.8125rem;
  color: var(--text);
  line-height: 1.55;
}

.spark-insights-card-cta .spark-insights-card-link {
  color: var(--text);
}

/* Override for the CTA slide — see .work-card-cta .work-card-link's comment
   for why this is needed, not optional. */
/* stylelint-disable-next-line no-duplicate-selectors */
.spark-insights-card-cta .spark-insights-card-link {
  margin-top: 0;
}

.spark-insights-dots {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 1px;
  margin-top: 10px;
}

/* 24x24 hit area, 8px painted dot -- see .work-dot for the reasoning. */
.spark-insights-dot {
  width: 24px;
  height: 24px;
  padding: 0;
  border: none;
  background: none;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}
.spark-insights-dot::before {
  content: "";
  width: 8px;
  height: 8px;
  border-radius: 999px;
  background: var(--chip-border);
  transition: background 0.25s ease, transform 0.25s ease;
}
.spark-insights-dot.is-current::before {
  background: var(--accent-core);
  transform: scale(1.35);
}
.spark-insights-dot:focus-visible {
  outline: none;
}
.spark-insights-dot:focus-visible::before {
  outline: 2px solid var(--accent-core);
  outline-offset: 3px;
}

/* Section "keep reading" links -- #why-us -> /why-us/, #faq -> /faq/,
   #about -> /about/ (2026-08-25, per request: "for anywhere we have these
   more links lets style them better so they blend in and look polished
   instead of random text. maybe use icons if it suits it").

   They were a bare coloured word sitting centred under a section with no
   affordance at all -- nothing marked them as the way onward rather than an
   incidental link inside the copy, which on a page whose whole job is to move
   people forward is the wrong thing to leave undecorated.

   Now a pill, in the site's own existing language rather than a new one:
   999px radius and 1px --border are what .btn, .eyebrow and .filter-tab
   already use, so this reads as a quieter member of the button family. It is
   deliberately NOT filled -- these compete with the section's real CTA
   ("Book a Call", the contact form), and a second solid lime button per
   section would flatten that hierarchy. Ghost styling says "there is more
   here" without shouting over the thing we actually want clicked.

   The arrow is a CSS ::after glyph, not an <svg> or an icon font: it is
   decorative, it must never be read aloud (the link text already says where
   it goes), and a pseudo-element is invisible to assistive tech for free.
   It nudges 3px on hover -- the one bit of motion, gated on
   prefers-reduced-motion below. */
.industries-note a {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  padding: 11px 20px;
  border: 1px solid var(--border);
  border-radius: 999px;
  color: var(--accent-core-text);
  font-weight: 700;
  font-size: 0.875rem;
  line-height: 1;
  text-decoration: none;
  /* nowrap so the pill keeps its shape: without it the longest label ("More
     about us and how we work") broke across three lines inside the rounded
     border, turning a button into a blob -- measured at 132x103 before this.

     max-width is the guard that makes nowrap safe. nowrap alone forces the
     element as wide as its text regardless of the container, and at 320px
     that widest pill (313px) pushed 53px of horizontal scroll onto the whole
     DOCUMENT -- measured, after this rule was first written without it. */
  white-space: nowrap;
  max-width: 100%;
  transition: border-color 0.2s ease, background 0.2s ease, transform 0.2s ease;
}

/* Below ~420px the longest label genuinely cannot fit on one line, so let it
   wrap rather than clip or overflow. Centring keeps a two-line pill looking
   deliberate instead of ragged.

   Its OWN media query, colocated with the rules it modifies, rather than
   appended to one of the file's existing 480px blocks -- there are already
   two of those and a second pair at 900px, which is exactly how the
   .hero p.sub edit came to be silently overridden. A unique condition next to
   the thing it overrides cannot be shadowed by an earlier block. */
@media (max-width: 420px) {
  .industries-note a {
    white-space: normal;
    text-align: center;
  }
}
.industries-note a::after {
  content: "→"; /* → */
  font-size: 1.0625em;
  line-height: 1;
  transition: transform 0.2s ease;
}

.industries-note a:hover {
  border-color: var(--accent-core);
  /* color-mix keeps this tied to the brand token rather than hard-coding a
     tint that would drift if --accent-core ever changes, and the alpha is
     low enough to read as a wash in BOTH themes rather than a fill. */
  background: color-mix(in srgb, var(--accent-core) 10%, transparent);
  transform: translateY(-1px);
}
.industries-note a:hover::after {
  transform: translateX(3px);
}

.industries-note a:focus-visible {
  outline: 2px solid var(--accent-core);
  outline-offset: 3px;
}

@media (prefers-reduced-motion: reduce) {
  .industries-note a,
  .industries-note a::after {
    transition: none;
  }
  .industries-note a:hover {
    transform: none;
  }
  .industries-note a:hover::after {
    transform: none;
  }
}
/* ---------- CTA banner ---------- */
/* REFRESHED 2026-08-26, per request ("the ready to talk about your website
   green box doesn't look visually appealing ... polish and refresh this").

   What was wrong with it, specifically:
     - A flat --accent-core fill at 50px padding is a large rectangle of one
       very saturated colour with nothing happening in it. At the size this
       renders (full column width, ~180px tall) that reads as an unstyled
       block rather than as a designed panel.
     - The button was `background: var(--bg)`, which is #F7FBEF in light mode
       and #0A0A0A in dark -- so the site's single most important button was
       near-white on lime for half of visitors and near-black on lime for the
       other half. Not a variant; two different buttons.
     - Nothing tied it to the brand. The mark is lime-on-black, and this was
       the one place on the page big enough to say so.

   The gradient runs --accent-highlight -> --accent-core ONLY, and must keep
   doing so. It is tempting to end it on --accent-deep for more depth, and
   that would break the text: the body copy here is --accent-on (#0A0A0A) and
   measures 11.9:1 on --accent-core and 15.6:1 on --accent-highlight, but only
   about 2.4:1 on --accent-deep. Every stop this gradient touches has to carry
   near-black text.

   isolation:isolate + overflow:hidden are for the ::before glow below -- it
   is a blurred radial that would otherwise paint outside the rounded corners
   and over the section that follows. */
.cta-banner {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  background: linear-gradient(135deg, var(--accent-highlight) 0%, var(--accent-core) 62%);
  color: var(--accent-on);
  /* Matches .section/.hero's 1300px-capped, centered column (40px floor
     gutter below that, growing to center above it) — previously a flat
     "0 40px", so on wide monitors this stretched edge-to-edge while every
     .section above/below it stayed boxed at 1300px. */
  margin: 0 max(40px, 50% - 650px) 60px;
  border-radius: 22px;
  padding: 52px 50px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 32px;
  flex-wrap: wrap;
  /* A green-tinted shadow rather than a neutral one, so the panel looks lit
     from its own colour instead of pasted onto the page. */
  box-shadow: 0 18px 44px -22px rgba(92, 140, 11, 0.55);
}

/* The soft highlight in the top-left corner. One blurred radial, no image, no
   animation -- this element is on every page of the site and the aurora that
   used to sit in the hero was removed precisely because a permanently
   compositing blurred layer costs real frames. A static radial-gradient
   background costs one paint. */
.cta-banner::before {
  content: "";
  position: absolute;
  inset: -40% 40% 30% -20%;
  background: radial-gradient(closest-side, rgba(255, 255, 255, 0.55), rgba(255, 255, 255, 0));
  z-index: -1;
  pointer-events: none;
}

/* The spark bolt, oversized and cropped by the panel's own overflow -- the
   same mark the hamburger uses, so the closing CTA on every page carries the
   brand rather than just its colour. Deliberately very low contrast: it is
   texture, and anything more legible would compete with the heading. */
.cta-banner::after {
  content: "";
  position: absolute;
  right: -34px;
  bottom: -56px;
  width: 220px;
  height: 220px;
  z-index: -1;
  pointer-events: none;
  opacity: 0.13;
  background: currentcolor;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M13 2 4.5 13.5H11l-1 8.5L19.5 10H13z'/%3E%3C/svg%3E") no-repeat center/contain;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M13 2 4.5 13.5H11l-1 8.5L19.5 10H13z'/%3E%3C/svg%3E") no-repeat center/contain;
}

/* max-width in ch, not px: this block holds one heading and one sentence, and
   at 1300px wide with no cap the sentence ran to ~110 characters, which is
   roughly twice a comfortable measure. */
.cta-banner > div:first-child {
  max-width: 34rem;
  min-width: 0;
}

.cta-banner h2 {
  font-size: 1.75rem;
  font-weight: 700;
  margin-bottom: 10px;
  line-height: 1.2;
}

.cta-banner p {
  font-size: 0.9375rem;
  line-height: 1.6;
  opacity: 0.82;
}

/* Ink, not var(--bg). The wordmark is lime on near-black, so a near-black
   button with a lime label on a lime panel is the brand's own relationship
   rather than an inversion invented for this one element -- and unlike
   var(--bg) it is the SAME button in both themes (--accent-on is defined once
   in :root and never overridden). --accent-highlight on #0A0A0A measures
   13.9:1. */
.cta-banner .btn-primary {
  background: var(--accent-on);
  color: var(--accent-highlight);
  padding: 13px 26px;
  font-size: 0.9375rem;
  box-shadow: 0 10px 22px -12px rgba(10, 10, 10, 0.7);
}
.cta-banner .btn-primary:hover {
  box-shadow: 0 14px 26px -12px rgba(10, 10, 10, 0.8);
}

/* Reassurance line under the banner button (2026-08-18 CTA audit, finding 8).
   The hero's .hero-cta-note friction-killer existed on the homepage only, so
   every closing CTA on the site asked for a booking with nothing answering
   "am I about to get sold to". .cta-banner-action wraps button + note so the
   note tracks the button inside the banner's flex row rather than becoming a
   third free-floating flex child. */
.cta-banner-action {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 8px;
}

.cta-banner .cta-banner-note {
  font-size: 0.75rem;
  font-weight: 600;
  opacity: 0.8;
  text-align: center;
  margin: 0;
}

/* ---------- Services list ----------
   2026-08-19, per request: replaced the services accordion (hook + body
   paragraph + "Learn more" link, all now gone) with a plain numbered list --
   each row is itself the link to that service's subpage, so there's nothing
   left to expand. Same card/border/radius shape as the FAQ accordion items
   below for visual consistency (this page had exactly one "row card" look
   before this, and two rows of near-identical boxes with different
   interactions would read as inconsistent, not intentional), but its own
   classes rather than reusing .accordion-item -- this one is a static <a>,
   not a JS-driven open/close panel. */
.service-list {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.service-row {
  display: flex;
  align-items: center;
  gap: 18px;
  padding: 20px 22px;
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 10px;
  color: var(--text);
  transition: border-color 0.3s ease, box-shadow 0.3s ease;
}
.service-row:hover, .service-row:focus-visible {
  border-color: var(--accent-core);
  box-shadow: 0 0 0 1px var(--accent-core);
}

.service-num {
  font-weight: 900;
  font-size: 0.9375rem;
  color: var(--accent-core-text);
  flex-shrink: 0;
}

/* Same face/weight as .accordion-title strong (FAQ) -- see that rule's own
   comment for why this can't come from the page's general h1-h6 display-type
   rule: .service-name is a <span>, not a heading element at all. */
.service-name {
  flex: 1;
  font-family: var(--font-display);
  font-size: 1.0625rem;
  font-weight: 600;
}

.service-arrow {
  font-size: 1.125rem;
  color: var(--text-secondary);
  flex-shrink: 0;
  transition: transform 0.2s ease, color 0.2s ease;
}

.service-row:hover .service-arrow, .service-row:focus-visible .service-arrow {
  transform: translateX(4px);
  color: var(--accent-core-text);
}

/* ---------- FAQ accordion (JMD-style scrollHeight animation) ----------
   Services used to share this exact component (#servicesAccordion) until
   2026-08-19, when that section dropped the accordion/expand-collapse
   pattern entirely for a plain linked list (see .service-list above and
   front-page.php's #services docblock) -- these classes are FAQ-only now. */
/* No max-width here on purpose (was 820px) -- that made the accordion
   visibly narrower than every other section on the page (.timeline,
   .spark-grid, .cta-banner have none and fill the full .section width), so
   the page looked like it had two different content widths. The accordion
   bar itself now matches; readable line-length for the actual paragraph text
   is capped separately below, on .accordion-body p. */
/* .accordion-item is a native grouped <details name="faqAccordion"> — the
   browser opens/closes and enforces "only one open" on its own via the
   shared `name`, no JS controller left (kmcInitAccordion()/accordion.js
   removed, see git history). No max-height/transition on .accordion-body
   any more — <details> shows/hides it natively, instantly (Chromium alone
   can animate this smoothly via ::details-content + interpolate-size, so
   it's left as a plain box rather than a hybrid that would animate in one
   browser and snap in the other two). */
.accordion {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.accordion-item {
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 10px;
  overflow: hidden;
  transition: border-color 0.3s ease, box-shadow 0.3s ease;
}
.accordion-item[open] {
  border-color: var(--accent-core);
  box-shadow: 0 0 0 1px var(--accent-core);
}

/* .accordion .accordion-item-heading, NOT the bare class -- this is the THIRD
   time `.section h1, .section h2` (line ~1068) has quietly beaten a card-level
   heading rule, after .diff-card h3 and .insight-card-title, and this one had
   real cost: /faq/'s heading is an <h2>, so the bare class at (0,1,0) lost to
   (0,1,1) and every question in that accordion carried `margin-bottom: 30px`
   AND `font-size: 2.125rem`. The size was invisible (the <strong> inside sets
   its own) but the margin was not -- 30px of dead space under the text inside
   every collapsed card, measured as a 118px header around a 48px title at
   375px. The homepage's copy uses an <h3> and so was never hit, which is why
   this looked like a phone-only spacing quirk rather than a cascade bug.

   Ancestor-qualified at (0,2,0) rather than adding a tag, so it holds whether
   the markup uses h2 or h3 -- both are in use right now. `font: inherit` is
   the point of the rule: this element is a semantic wrapper and the visible
   title is .accordion-title strong. */
.accordion .accordion-item-heading {
  margin: 0;
  font: inherit;
}

.accordion-header {
  width: 100%;
  /* flex-start, not center: at phone width these questions run to two and
     three lines, and a vertically centred number floated down to the
     optical middle of the wrapped block instead of sitting on the line it
     indexes. The number and the plus are pinned to the first line by their
     own line-height below. */
  display: flex;
  align-items: flex-start;
  gap: 18px;
  background: transparent;
  border: none;
  padding: 20px 22px;
  cursor: pointer;
  text-align: left;
  font-family: var(--font-body);
  color: var(--text);
  list-style: none;
}
.accordion-header::-webkit-details-marker {
  display: none;
}

/* Fixed width AND tabular figures, or every question starts at a different x.
   The digits are proportional, so "01" measured 20px and "04" measured 24px,
   and the seven titles on /faq/ began at 101, 104, 104, 105, 104, 104 and 103
   -- a 4px stagger down a stack of seven cards, which reads as sloppy without
   being obviously wrong, which is why it survived every visual pass. Both are
   set: tabular-nums equalises the digits, min-width holds the column steady if
   the list ever crosses into three digits or the face loses its tnum feature.
   line-height 1.5 (15px * 1.5 = 22.5) matches the title's own 23px first line
   under the flex-start above. */
.accordion-num {
  font-weight: 900;
  font-size: 0.9375rem;
  line-height: 1.5;
  font-variant-numeric: tabular-nums;
  min-width: 26px;
  color: var(--accent-core-text);
  flex-shrink: 0;
}

.accordion-title {
  display: flex;
  flex-direction: column;
  gap: 4px;
  flex: 1;
}

/* The <h3> above this is `font: inherit` (line ~1007) — it's a semantic
   wrapper and deliberately carries no styling, so the display-type block's
   h1-h6 rule can't reach the visible title. Set the face here instead, at the
   same 600 the other card-level headings use, or this would be the only card
   heading on the page still in the body sans (.service-name below gets the
   same treatment for the same reason -- it took over this role for
   Services when that section dropped this component, 2026-08-19). */
.accordion-title strong {
  font-family: var(--font-display);
  font-size: 1.0625rem;
  font-weight: 600;
}

/* line-height 1.05 (22px * 1.05 = 23.1) puts the plus on the title's first
   line too, now that the header aligns to flex-start. */
.accordion-plus {
  font-size: 1.375rem;
  font-weight: 400;
  line-height: 1.05;
  transition: transform 0.25s ease;
  flex-shrink: 0;
}

.accordion-item[open] .accordion-plus {
  transform: rotate(45deg);
  color: var(--accent-core-text);
}

/* left offset matches the header's own padding-left (22) + num column (26) +
   gap (18) = 66, so the answer lines up under the QUESTION rather than under
   the number. It was 55, computed against an assumed 15px number column that
   the numbers never actually measured -- so the answer sat 5-9px to the LEFT
   of the question it belongs to, on every card. Keep this in step with
   .accordion-num's min-width; the two are one measurement.
   Only rendered while [open] (native <details>), so this is just the one
   static value now rather than a closed/open pair. */
.accordion-body {
  padding: 8px 22px 26px 66px;
}
.accordion-body p {
  color: var(--text-secondary);
  font-size: 0.875rem;
  line-height: 1.6;
  max-width: 640px;
}

/* The global `* { margin: 0 }` reset (see top of file) left every <p> in here
   flush against the next — the body paragraph, the "Learn more" link, and
   (on the SEO item) the "See where we work" link all sat with zero gap. */
.accordion-body p + p {
  margin-top: 10px;
}

.accordion-body a {
  color: var(--accent-core-text);
  font-weight: 700;
}

/* ---------- Process timeline ---------- */
.timeline {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 18px;
}

.timeline-step {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 14px;
  padding: 24px 20px 22px;
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 12px;
  position: relative;
  transition: border-color 0.3s ease, box-shadow 0.3s ease;
}
.timeline-step:hover {
  border-color: var(--accent-core);
  box-shadow: 0 10px 26px -14px rgba(0, 0, 0, 0.35);
}

.timeline-num {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background: var(--accent-core);
  color: var(--accent-on);
  font-weight: 900;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.3s ease;
}

.timeline-step:hover .timeline-num {
  transform: scale(1.15);
  box-shadow: 0 0 0 6px var(--accent-glow);
}

.timeline-step strong {
  font-size: 1rem;
  font-weight: 800;
  display: block;
  margin-bottom: 4px;
}

.timeline-step p {
  color: var(--text-secondary);
  font-size: 0.8125rem;
}

.contact-steps {
  margin-bottom: 40px;
}

.pricing-footnote {
  margin-top: 24px;
  font-size: 0.8125rem;
  color: var(--text-secondary);
}
.pricing-footnote a {
  color: var(--accent-core-text);
  font-weight: 700;
}

/* ---------- About ---------- */
.about-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 24px;
  margin-top: 10px;
}
.about-grid strong {
  font-size: 0.9375rem;
  font-weight: 800;
  display: block;
  margin-bottom: 6px;
}
.about-grid p {
  font-size: 0.8125rem;
  color: var(--text-secondary);
  line-height: 1.6;
}

/* /free-audit/'s own 5-card "What We Look At" grid (2026-08-20, per
   request -- "mismatching sized columns looks weird and not
   proportional"). 5 is prime: no CSS Grid column count except 1 or 5 ever
   divides it evenly, so widening minmax to force 3 columns (fixing the
   4-then-1 orphan at desktop) just moved the same problem to whatever
   width makes 2 columns fit instead (confirmed live: 2-then-2-then-1 at
   ~1000px) -- Grid lays every row into the SAME fixed column tracks, so
   an incomplete last row is always pinned left, however many columns
   there are. Flexbox doesn't have that constraint: justify-content:center
   on a wrapping flex row centers each line's own leftover space
   independently, so however many cards land in the last row, THAT row
   centers itself instead of trailing off at the left edge. Not a change
   to the shared .about-grid base (page-service-areas.php also uses it,
   for an unrelated county grid), same reasoning as before. */
.about-grid-audit {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
}
.about-grid-audit > div {
  flex: 1 1 380px;
  max-width: 417px;
}

/* v3 (2026-08-20, per request: "make the section full width"). Was capped
   at a fixed 640px text-block measure (2026-08-18 layout audit, matching
   .why-us-lede) on the reasoning that a single "Our roots" card down from
   the original 5-card grid would otherwise stretch across the section's
   full ~1300px width and become a hard-to-read line length -- a real
   tradeoff, not an oversight, but one the follow-up request overrides
   explicitly. Now behaves like .about-grid's own default (auto-fit,
   minmax(260px, 1fr)): the lone remaining card fills the section, matching
   the h2 above it and every other now-full-width element on this page
   (.section-lead went the same way 2026-08-18, for the same "one left
   edge, one right edge" reasoning). If the wider measure reads as harder
   to read at a glance, that's the tradeoff this override was told to
   accept -- worth flagging rather than quietly re-narrowing it back. */
.about-grid-single {
  grid-template-columns: minmax(260px, 1fr);
}

/* ---------- Contact: book-a-call iframe ---------- */
/* v2 (2026-08-18, per request): back to a direct iframe embed (see front-
   page.php's docblock) -- was briefly Google's "scheduling button" widget,
   reverted since a button-that-opens-a-popup didn't read as "a calendar is
   on this page" the way this inline embed does. 600px matches the height
   Google's own iframe snippet ships with; full-width so it fills
   .contact-book at any card width without a separate mobile override. */
.contact-book {
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 16px;
  padding: 34px;
}
.contact-book h2 {
  font-size: 1.5rem;
  margin-bottom: 10px;
}
.contact-book > p {
  color: var(--text-secondary);
  font-size: 0.9375rem;
  line-height: 1.6;
  max-width: 480px;
  margin-bottom: 22px;
}

.contact-book-iframe {
  width: 100%;
  height: 600px;
  border: 0;
  border-radius: 10px;
}

/* Locked-then-unlocked booking step (2026-08-18 v3 -- see front-page.php's
   #contactBooking docblock). The card is always visible now; .is-locked is
   what swaps the two copy states and shows the lock panel in place of the
   calendar. The iframe keeps its own [hidden] attribute (dropped by
   window.kmcRevealBooking() at the same time as .is-locked) so it stays out
   of the tab order and the a11y tree while it has no src to show.

   Everything is keyed off .is-locked rather than its absence, so the card
   renders in its unlocked shape if the class is ever removed by hand or the
   stylesheet loads without the JS. */
/* stylelint-disable-next-line no-duplicate-selectors */
.contact-book {
  margin-top: 34px;
  margin-bottom: 0;
}

/* camelCase, not kebab-case: matches getElementById('contactBooking') in
   main.js -- the codebase's real ID convention for JS-referenced elements,
   this is just the one such ID that also needs a CSS rule of its own. */
/* stylelint-disable-next-line selector-id-pattern */
#contactBooking {
  border-color: var(--accent-core);
}

.contact-book .eyebrow-kicker {
  margin-bottom: 14px;
}

.contact-book .book-state-locked, .contact-book.is-locked .book-state-open {
  display: none;
}

.contact-book.is-locked .book-state-locked {
  display: block;
}

/* h2's two spans are inline-level children of a block heading, so the
   display swap above needs no further layout handling -- but p.book-state-*
   carries .contact-book > p's own margin, so only one of the pair is ever in
   flow at a time. */
/* min-height, not height: the lock panel and the 600px calendar are very
   different shapes, and pinning the frame to the taller one would leave a
   340px hole under the lock copy. The panel sizes to its own content and the
   iframe overrides with its fixed height once it's in. */
.contact-book-frame {
  position: relative;
  min-height: 260px;
  display: flex;
}

.contact-book-lock {
  flex: 1;
  display: none;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 10px;
  text-align: center;
  padding: 34px 24px;
  border: 1px dashed var(--chip-border);
  border-radius: 10px;
  background: var(--bg);
}

.contact-book.is-locked .contact-book-lock {
  display: flex;
}

.contact-book-lock-icon {
  font-size: 1.75rem;
  line-height: 1;
  opacity: 0.8;
}

/* Same card-title convention as .why-us-teaser-card h3 -- display face, 600.
   Was Inter 800. */
.contact-book-lock-title {
  font-family: var(--font-display);
  font-size: 1rem;
  font-weight: 600;
  color: var(--text);
  margin: 0;
}

.contact-book-lock-note {
  font-size: 0.8125rem;
  color: var(--text-secondary);
  line-height: 1.6;
  max-width: 420px;
  margin: 0;
}

.contact-book-lock .btn {
  margin-top: 8px;
}

/* Adds flex-item sizing for when the iframe sits in the lock panel's flex
   layout — no overlap with the earlier width/height/border/border-radius
   block above. */
/* stylelint-disable-next-line no-duplicate-selectors */
.contact-book-iframe {
  flex: 1;
}

/* Fallback caption below the calendar (2026-08-27, usability test finding --
   see front-page.php's docblock right above this element's markup). Overrides
   .contact-book > p's default 0.9375rem/480px/22px -- this is a small
   secondary note under an already-large iframe, not a lead paragraph, so it
   matches .contact-book-lock-note's smaller size instead. margin-top rather
   than relying on the inherited margin-bottom, since this sits below the
   frame rather than above it. */
.contact-book > p.contact-book-fallback {
  font-size: 0.8125rem;
  margin: 12px 0 0;
  max-width: none;
}

.contact-book-fallback a {
  color: var(--accent-core);
}

/* UNUSED as of 2026-08-18 -- the "or send us your project details" divider
   went away when #contact stopped offering booking and the form as two peer
   options (see front-page.php's #contact docblock). Kept, not deleted, in
   case the section ever goes back to a two-path layout. */
.contact-divider {
  display: flex;
  align-items: center;
  gap: 16px;
  margin: 0 0 34px;
  color: var(--text-secondary);
  font-size: 0.75rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}
.contact-divider::before, .contact-divider::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--border);
}

/* ---------- Contact ---------- */
/* Single column, full width (2026-08-19, per request) -- was a 2-column grid
   (info sidebar | form), which the max-width:700px breakpoint below already
   overrode to one column on mobile. This makes that the ONLY layout: .contact-
   info (heading, intro, phone/email/address) now reads as the form's own
   header, stacked above it and left-aligned same as everywhere else on the
   page (nothing here centers it) rather than living in a side column. */
/* CARD (2026-08-25, per request: "I think the form should have a better
   container and visually leading to it").

   Before this the form was the only major interactive block on the site with
   no container at all -- labels and inputs sitting directly on the page
   background, with nothing marking where the section stopped and the thing
   you are meant to DO started. Every other comparable block already had one
   (.audit-card, .included-card, .content-card, all 1px border + --card-bg +
   16px radius), so this is the page's own existing convention finally applied
   to the one element every CTA on the site points at, not a new treatment.

   Styling .contact-layout rather than .contact-form-col is deliberate: this
   wrapper holds the heading, the intro, the form AND #contactFormSuccess, so
   the card keeps its shape through the submit -- the confirmation panel lands
   inside the same box the form occupied instead of the container vanishing at
   the moment of conversion. #contactBooking is a SIBLING of this element, not
   a child (verified against the markup), so the calendar that reveals after a
   successful submit correctly stays outside the card as its own next step.

   The accent top rule is the "leading to it" half. It reads as the section
   heading pointing down into the box, and it is the same accent every primary
   button on the page uses, so the eye already associates it with "this is the
   action". 3px, full-bleed across the card's top edge, clipped by the radius
   via overflow: hidden on the ::before rather than a border-top (a border
   would sit outside the radius and square off the corners). */
.contact-layout {
  display: flex;
  flex-direction: column;
  gap: 32px;
  position: relative;
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 16px;
  padding: 36px 34px 34px;
  overflow: hidden;
}
.contact-layout::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 3px;
  background: var(--accent-core);
}

/* The card supplies the separation the 30px heading margin used to have to
   provide on its own, and the accent rule needs to read as attached to the
   heading above it rather than floating. */
#contact > h2 {
  margin-bottom: 18px;
}

/* .contact-intro is gone (2026-08-26) with the paragraph it styled -- see
   front-page.php. Its mobile size bump and its padding-right went with it. */
/* No max-width here (was 640px) -- same full-width pass as the hero: this
   left the form narrower than its own grid column (.contact-layout's 1fr
   side), which itself already spans the full section width. */
/* Wrapper added 2026-08-18 so the submit-confirmation panel can take the
   form's place without becoming a third .contact-layout grid child (which
   would have added a column). The column itself carries no styling -- the
   form and the panel are only ever shown one at a time. */
.contact-form-col {
  min-width: 0;
}

/* scroll-margin-top: the mid-page #audit hook scrolls to THIS element, not
   to #contact, so `section[id] { scroll-margin-top: 90px }` further down
   this file does not apply -- without it the form's top (and the
   .form-audit-request banner with it) landed underneath the sticky header.
   Same 90px as that rule, deliberately: it is clearing the same header. */
.contact-form {
  display: flex;
  flex-direction: column;
  gap: 16px;
  scroll-margin-top: 90px;
}

/* `.contact-form`'s own `display: flex` (an author rule) otherwise beats the
   browser's default `[hidden] { display: none }` (a same-specificity but
   lower-priority user-agent rule) -- so main.js's `contactForm.hidden = true`
   on submit was a no-op and the whole form, submit button included, stayed
   on screen floating above #contactFormSuccess. This is what actually made
   "the form and the panel are only ever shown one at a time" above true. */
.contact-form[hidden] {
  display: none;
}

.form-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 16px;
}

/* min-width:0 on both the label (a .form-row grid item) and the controls
   inside it (flex items of the label). Without it, min-width defaults to
   `auto` at both levels, so a <select> is as wide as its longest <option>
   and simply refuses to shrink -- which is how the 2026-08-18 challenge
   list ("Stuck on a DIY builder or template") put 11px of horizontal
   overflow on the whole document at 375px, found while verifying that
   change. Nothing about the markup made this visible; only measuring
   document.scrollWidth did. */
/* The label is the flex COLUMN (caption, control, and any appended
   .field-error-msg). Its font rules therefore reached all three, which is why
   the caption could never be styled on its own -- see .field-caption below,
   which now owns it. What is left here governs the error message and any
   label that has no caption wrapper. */
.contact-form label {
  display: flex;
  flex-direction: column;
  gap: 6px;
  min-width: 0;
  font-size: 0.75rem;
  font-weight: 700;
  color: var(--text-secondary);
  transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
}

/* The caption itself (2026-08-26, per request: the field titles "look like
   they're not using the same font").

   They did not, exactly -- they were 0.75rem/700 in --text-secondary, which
   is indistinguishable from a browser's own small grey form label, and they
   sat next to a site whose every other small caption is a deliberate tracked
   uppercase label. Not uppercase here on purpose: "How did you hear about
   us?" is a question, and micro-caps make questions harder to read, not more
   designed. Full --text instead of --text-secondary is the bigger half of
   the fix -- with no placeholders on this form (deliberate, see the partial),
   the caption is the ONLY thing naming the field, so it should not be the
   quietest text in its own row. */
.field-caption {
  display: flex;
  align-items: baseline;
  gap: 8px;
  font-size: 0.8125rem;
  font-weight: 700;
  letter-spacing: 0.005em;
  color: var(--text);
}

/* "(optional)" as part of the label, NOT a badge (2026-08-26, per request:
   "maybe we dont need to say optional in words there? whatever the best
   modern solution is").

   THE WORDS STAY; the badge goes. An asterisk on the required fields is the
   obvious alternative and it is the weaker one: an asterisk means nothing to
   a first-time visitor unless a legend explains it, screen readers announce
   it inconsistently, and an UNMARKED field then becomes ambiguous -- did they
   mean it is required, or did they forget to mark it? Both the GOV.UK Design
   System and NN/g land on marking the optional fields in words for exactly
   that reason. Six of the nine fields here are optional, so this is also the
   difference between "which can I skip" being answerable at a glance and
   being guesswork.

   What was actually wrong was the STYLING, not the copy: 11px uppercase at
   800 with 0.12em tracking is this site's badge treatment, so it read as a
   status chip stuck onto the caption rather than as part of it. Now the same
   size and weight as the caption's own text, lowercase, parenthesised, one
   step down in colour -- the label reads as one phrase, and the marker is
   quiet enough to skip and clear enough to find. */
.field-optional {
  font-style: normal;
  font-size: 0.75rem;
  font-weight: 500;
  letter-spacing: normal;
  text-transform: none;
  color: var(--text-secondary);
  flex: none;
}

/* The marker must not survive into the error state: a field that just failed
   validation showing a grey "OPTIONAL" beside its red caption is a straight
   contradiction. Only the required fields can reach .field-error, so in
   practice this is belt and braces -- but it is one line. */
.contact-form label.field-error .field-optional {
  display: none;
}

.contact-form label.field-pop {
  animation: labelPop 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
}

@keyframes labelPop {
  0% {
    transform: scale(1);
  }
  45% {
    transform: scale(1.08);
    color: var(--accent-core);
  }
  100% {
    transform: scale(1);
  }
}
/* The consent checkbox is a ROW, not a stacked field. `.contact-form label`
   is flex-column with a 0.75rem bold caption, which is right for "Email"
   sitting above an input and wrong for a sentence somebody has to read and
   agree to -- bold micro-caps is the last thing consent copy should be set
   in. The input also has to opt out of the text-field styling directly
   below, which would otherwise give the box 11px of padding and an 8px
   radius and stop it rendering as a checkbox at all. */
/* v2 (2026-08-26): a bordered panel, not a line of small print. Unstyled, it
   occupied the one position in a form -- between the last field and the
   submit button -- that readers have been trained to skip, and it was set in
   the same quiet grey a disclaimer would be. The border and the --field-bg
   fill borrow the form's own field treatment, so it reads as part of the
   form rather than as a footnote under it, and the hover/checked border
   gives it the same feedback every other control here has. */
.contact-form label.form-optin {
  flex-direction: row;
  align-items: flex-start;
  gap: 12px;
  padding: 14px 16px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--field-bg);
  font-size: 0.8125rem;
  font-weight: 400;
  line-height: 1.5;
  color: var(--text-secondary);
  cursor: pointer;
  transition: border-color 0.2s ease;
  /* 640px rather than the form's full 1115px (it matched .contact-intro-row
     when that row still held a paragraph; that measure now lives only here). Uncapped, the body sentence set as
     ONE ~150-character line -- roughly twice a comfortable measure, and on
     the one block here somebody has to read before agreeing to it. This is
     the only element on the form whose job is reading rather than typing,
     so it is the only one that wants a measure. */
  max-width: 640px;
}
.contact-form label.form-optin:hover {
  border-color: var(--accent-core);
}

/* :has(), so the panel confirms the tick without JS. Every browser this site
   supports has shipped it since 2023; where it is missing the checkbox's own
   accent-color still shows the state, so this is enhancement, not the only
   signal. */
.contact-form label.form-optin:has(input:checked) {
  border-color: var(--accent-core);
}

/* The description drops to 0.75rem (2026-08-26, per request: "make the
   description smaller font"). The two lines were the same 0.8125rem as the
   title above them and separated from it only by weight, so the panel read as
   one undifferentiated block -- the bold line did not lead, it just started.
   A step down gives the title something to be bigger than.

   0.75rem is --pill-size, i.e. a size this theme already uses; it is not a
   sixth ad-hoc value. Line-height stays 1.5 off the label, so the two lines
   set at 18px and the checkbox alignment below is unaffected.

   NOT ITALIC, though it was asked for as a "maybe". Only the UPRIGHT faces of
   Bitter and Inter are vendored (assets/fonts/fonts.css declares
   font-style: normal on all four @font-face rules), so font-style: italic
   here produces a SYNTHETIC italic -- the browser mechanically slanting the
   upright -- which is the same class of defect as the synthetic bolding this
   theme audited out on 2026-08-26. Doing it properly means vendoring
   Inter's italic subset (a separate ital,wght woff2 from Google Fonts, plus
   its OFL notice) and adding a fifth @font-face; until then upright is the
   honest rendering. */
.form-optin-text {
  font-size: 0.75rem;
}

/* Held at the .field-caption size the rest of this form's labels use --
   .form-optin-text above would otherwise pull it down to 0.75rem too, which
   is the whole distinction this pair now depends on. */
.form-optin-title {
  display: block;
  font-size: 0.8125rem;
  font-weight: 700;
  color: var(--text);
  margin-bottom: 3px;
}

.contact-form label.form-optin input[type=checkbox] {
  flex: 0 0 auto;
  width: 16px;
  height: 16px;
  /* 3px, not 1px: the first line of this block is now the bold title at
     line-height 1.5, so the box centres on that line rather than sitting
     above it. */
  margin: 3px 0 0;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: none;
  /* Brand green as a control FILL, never as text -- it measures 1.85:1 on
     this background and is unreadable as a foreground colour. */
  accent-color: var(--accent-core);
  cursor: pointer;
}

/* See --field-bg's note in :root for why these are not --card-bg any more.
   The border went up a step too: at 1px of --border on an identical fill the
   field had no edge to speak of, and thickening the line was pointless while
   the two surfaces matched -- both halves had to change together. */
.contact-form input, .contact-form select, .contact-form textarea {
  min-width: 0;
  max-width: 100%;
  font-family: var(--font-body);
  font-size: 0.875rem;
  padding: 12px 14px;
  border-radius: 10px;
  border: 1px solid var(--field-border);
  background: var(--field-bg);
  color: var(--text);
  transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.3s ease, border-color 0.25s ease;
}

/* No scale on focus for the textarea (unlike the inputs above): it's the
   tallest control on the form, so the same 1.025 nudge visibly shoves the
   submit button rather than reading as a pop. resize:vertical because a
   horizontal drag would break out of the grid column. */
.contact-form textarea {
  line-height: 1.6;
  resize: vertical;
  min-height: 96px;
}

/* A hover state, which these had none of. On a form where the fill and the
   card were the same colour there was nothing to hover; now that a field is a
   distinct surface, reacting to the cursor is what tells somebody it is one. */
.contact-form input:hover, .contact-form select:hover, .contact-form textarea:hover {
  border-color: var(--chip-border);
}

.contact-form input:focus, .contact-form select:focus, .contact-form textarea:focus {
  outline: none;
  transform: scale(1.025);
  border-color: var(--accent-core);
  box-shadow: 0 0 0 4px var(--accent-glow);
}

.contact-form textarea:focus {
  transform: none;
}

.contact-form input.spark-burst, .contact-form select.spark-burst {
  animation: sparkRing 0.5s ease;
}

@keyframes sparkRing {
  0% {
    box-shadow: 0 0 0 0 var(--accent-glow);
  }
  100% {
    box-shadow: 0 0 0 16px rgba(0, 0, 0, 0);
  }
}
.form-spark {
  position: absolute;
  pointer-events: none;
  font-size: 1.125rem;
  color: var(--accent-core);
  z-index: 999;
  animation: sparkPop 0.7s ease forwards;
}

@keyframes sparkPop {
  0% {
    opacity: 0;
    transform: translateY(0) scale(0.5) rotate(0deg);
  }
  30% {
    opacity: 1;
    transform: translateY(-16px) scale(1.25) rotate(15deg);
  }
  100% {
    opacity: 0;
    transform: translateY(-38px) scale(0.85) rotate(-12deg);
  }
}
.contact-form button {
  align-self: flex-start;
  margin-top: 6px;
}
.contact-form button.btn-pop {
  animation: btnPop 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
}

@keyframes btnPop {
  0% {
    transform: scale(1);
  }
  40% {
    transform: scale(1.08);
  }
  100% {
    transform: scale(1);
  }
}
.contact-form button.btn-success {
  background: var(--accent-highlight);
  color: var(--bg);
}

/* Reassurance line right under the submit button (CONV-05, pre-promotion
   audit). Same treatment as .hero-cta-note -- small, muted, pulled tight
   under the control above it rather than using the form's own 16px gap,
   which would read as a new, separate line rather than a footnote on the
   button. Left-aligned, matching the button's own align-self: flex-start;
   .cta-banner-note is centred because banner buttons there are centred too. */
.form-submit-note {
  margin: -4px 0 0;
  font-size: 0.8125rem;
  color: var(--text-secondary);
}

/* Challenge cycled through checkbox chips, a custom dropdown, and a native
   <select multiple> on 2026-08-19 before being reverted back to a plain
   single-select the same day (see front-page.php's docblock) -- no
   field-specific CSS needed any more; it's fully covered by the shared
   ".contact-form input, .contact-form select, .contact-form textarea" rule
   like every other select on this form. */
/* ---------- Contact: heading row + sound toggle ---------- */
/* The toggle is pulled OUT of this row (2026-08-25, per request: "move the
   speaker icon to a corner or somewhere its propelry aligned").

   It used to be the second child of this flex row under
   `justify-content: space-between` with `max-width: 640px` on the row -- so
   it was pinned to the 640px mark. That was fine while the form column was
   640px wide; once #contact became a full-width card (same day) the icon was
   left floating in open space beside the heading, aligned to an edge that no
   longer exists. Now it is absolutely positioned to the CARD's top-right
   corner, which is a real edge and the conventional place for a secondary
   toggle. .contact-layout is already position:relative, so it is the
   containing block. */
/* The card's own <h2> is GONE (2026-08-26, see front-page.php for why) --
   this section carried two of them seven lines apart, and the card's one
   restated both the section heading above it and the paragraph below it.
   What is left in this row is the intro paragraph and the toggle, so the row
   is named for the paragraph now.

   The sizing pass this comment used to describe (h2 down from the sitewide
   2.125rem to 1.625rem, "~40px off the top of the form") is SUPERSEDED
   rather than reverted -- removing the line beats shrinking it. The eyebrow
   and lead margins it tightened in the same pass are kept. */
/* All this row holds now is the sound toggle, which is out of flow at
   desktop -- so the row is zero-height there, and its old 26px bottom margin
   was 26px of nothing between the eyebrow block and the form. Below 720px the
   toggle drops back in and the row becomes the line it sits on. */
.contact-intro-row {
  display: flex;
  align-items: flex-start;
  justify-content: flex-end;
}

#contact > .eyebrow-kicker {
  margin-bottom: 12px;
}

#contact > .section-lead {
  margin-top: 14px;
  margin-bottom: 22px;
  max-width: 60ch;
}

/* The .contact-intro-row .contact-intro rule that used to sit here kept the
   paragraph clear of the absolutely-positioned toggle. There is no paragraph
   any more. */
.sound-toggle {
  position: absolute;
  top: 20px;
  right: 20px;
  z-index: 2;
  flex: none;
  width: 38px;
  height: 38px;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: transparent;
  color: var(--text);
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.9375rem;
  transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.3s ease;
}
.sound-toggle:hover {
  box-shadow: 0 0 0 6px var(--accent-glow);
}
.sound-toggle.muted {
  opacity: 0.5;
}

/* ---------- Contact: progress dots ---------- */
/* Moved into the card's top-right, beside the sound toggle (2026-08-25, per
   request: "properly align/move the progress dots").

   They were sitting in the flow between the intro paragraph and the first
   field, on a -4px top margin -- a row of eight unlabelled circles closer to
   the fields than to anything that explained them, reading as decoration
   rather than as progress. Both of these are form CHROME (one reports
   progress, one controls the form's sound), so they belong together and out
   of the reading path, not interrupting it.

   Absolutely positioned rather than moved in the markup, deliberately:
   main.js finds these with `contactForm.querySelectorAll( '#formProgress
   .form-progress-dot' )`, so #formProgress has to stay INSIDE the form
   element. Positioning is the only way to co-locate it with a control that
   lives in .contact-info without breaking that selector.

   Vertically centred against the 38px toggle: 20px inset + (38 - 9) / 2. */
.form-progress {
  position: absolute;
  top: 34px;
  right: 70px;
  z-index: 2;
  display: flex;
  align-items: center;
  gap: 9px;
  margin: 0;
}

.form-progress-dot {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: transparent;
  border: 1.5px solid var(--chip-border);
  transition: background 0.3s ease, border-color 0.3s ease, transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.3s ease;
}
.form-progress-dot.filled {
  background: var(--accent-core);
  border-color: var(--accent-core);
  animation: dotFill 0.9s cubic-bezier(0.16, 1, 0.3, 1);
}

@keyframes dotFill {
  0% {
    transform: scale(1);
    box-shadow: 0 0 0 0 var(--accent-glow);
  }
  50% {
    transform: scale(1.25);
    box-shadow: 0 0 0 6px var(--accent-glow);
  }
  100% {
    transform: scale(1);
    box-shadow: 0 0 0 0 rgba(0, 0, 0, 0);
  }
}
.contact-form label.field-complete input, .contact-form label.field-complete select {
  border-color: var(--accent-core);
}

.form-submit-error {
  margin: 10px 0 0;
  padding: 10px 14px;
  border-radius: 8px;
  background: rgba(220, 68, 68, 0.1);
  border: 1px solid rgba(220, 68, 68, 0.35);
  color: #dc4444;
  font-size: 0.875rem;
}

/* ---------- Contact: submit confirmation ---------- */
/* Replaces the form on a successful submit (2026-08-18) -- see
   front-page.php's #contactFormSuccess docblock. Accent border and check
   mark rather than the form's neutral card, so it reads as "this worked"
   at a glance from anywhere on the section. */
.form-success {
  border: 1px solid var(--accent-core);
  background: var(--card-bg);
  border-radius: 16px;
  padding: 30px;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 12px;
}

/* Same bug as .contact-form above, same fix: this rule's own `display: flex`
   otherwise beats the browser's default `[hidden] { display: none }`, so the
   success panel was showing PERMANENTLY on every page load -- sitting right
   under the blank, never-submitted form -- not just after a real submit.
   That's what the "spacing" report two rounds ago actually was: the form
   was never hidden AND the success panel was never hidden either, both
   rendering unconditionally, stacked. */
.form-success[hidden] {
  display: none;
}

.form-success-check {
  width: 38px;
  height: 38px;
  border-radius: 50%;
  background: var(--accent-core);
  color: var(--accent-on);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.125rem;
  font-weight: 800;
}

.form-success h3 {
  font-size: 1.25rem;
  font-weight: 600;
  margin: 0;
}

.form-success p {
  color: var(--text-secondary);
  font-size: 0.9375rem;
  line-height: 1.6;
  margin: 0;
  max-width: 460px;
}

/* Element+class so it outweighs `.form-success p`'s margin: 0 on
   specificity rather than needing !important. */
.form-success p.form-success-book {
  padding-top: 14px;
  border-top: 1px solid var(--border);
  width: 100%;
  margin-top: 6px;
  max-width: none;
}

.form-success .btn {
  margin-top: 4px;
}

.form-success:focus {
  outline: none;
}

.form-success:focus-visible {
  outline: 3px solid var(--text);
  outline-offset: 3px;
}

/* ---------- Contact: per-field validation ---------- */
/* 2026-08-18, per request ("make sure the form required fields validation
   highlights which fields are missing"). Both forms carry `novalidate`, so
   the browser's own bubble never fires and, before this, a missing required
   field was only caught server-side -- Fluent Forms returned one message for
   one field into .form-submit-error, with nothing on the page indicating
   WHICH field it meant. main.js now validates on submit and marks every
   offending label; the same marker is reused for field-keyed errors that
   come back from the server.

   The red is hardcoded to the same #dc4444 .form-submit-error above already
   uses -- it's the only red on the site and it's identical in both themes.
   Promoting it to a :root token would mean touching that existing rule too,
   which isn't this change's job. */
.contact-form label.field-error {
  color: #dc4444;
}
.contact-form label.field-error input, .contact-form label.field-error select {
  border-color: #dc4444;
  background: rgba(220, 68, 68, 0.04);
}

/* Overrides .contact-form input:focus's accent glow -- an invalid field
   should still read as invalid while it has focus, otherwise the highlight
   disappears exactly when the visitor goes to fix it. */
.contact-form label.field-error input:focus,
.contact-form label.field-error select:focus {
  border-color: #dc4444;
  box-shadow: 0 0 0 4px rgba(220, 68, 68, 0.18);
}

/* field-complete's accent border would otherwise win on source order for a
   field that was filled, then emptied again. */
.contact-form label.field-complete.field-error input,
.contact-form label.field-complete.field-error select {
  border-color: #dc4444;
}

.field-error-msg {
  display: flex;
  align-items: center;
  gap: 5px;
  font-size: 0.6875rem;
  font-weight: 700;
  color: #dc4444;
  margin-top: -1px;
}
.field-error-msg::before {
  content: "!";
  flex: none;
  width: 13px;
  height: 13px;
  border-radius: 50%;
  background: #dc4444;
  color: #fff;
  font-size: 0.5625rem;
  line-height: 13px;
  text-align: center;
}

/* ---------- Contact: submit celebration firework burst ---------- */
.firework-particle {
  position: absolute;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  pointer-events: none;
  z-index: 999;
  box-shadow: 0 0 6px 1px currentcolor;
  animation: fireworkBurst 950ms cubic-bezier(0.16, 1, 0.3, 1) forwards;
}

@keyframes fireworkBurst {
  0% {
    transform: translate(0, 0) scale(1);
    opacity: 1;
  }
  65% {
    opacity: 1;
  }
  100% {
    transform: translate(var(--tx), var(--ty)) scale(0.3);
    opacity: 0;
  }
}
/* ---------- Footer ---------- */
/* v3 (2026-08-18) — labelled 4-column grid, replacing v2's single
   space-between flex row. See footer.php's own docblock for why. The old
   .footer-row / .footer-brand-as-flex-row / .footer-links rules are gone
   with it, including the vertical-rule ::before that separated the brand
   block from the links and the mobile 3-column link grid that stood in for
   it — column headings do that job now, at every width.

   Same 1300px-capped, centered column as .section/.cta-banner. */
/* The footer body follows the page theme again (2026-08-26 v7). A short-lived
   version painted the WHOLE footer near-black; that colour moved to the signup
   band instead, which is where the reference layout puts it -- one coloured
   band over a plain body. A dark body also fought .cta-banner, the lime
   gradient sitting directly above this on every subpage. */
/* --footer-rule-gap is the clearance EVERY horizontal rule in this footer gets
   on BOTH sides (2026-08-26, per request: "fix divider spacing in footer").

   The four rules disagreed. The three inside the grid measured 26px above and
   26 below -- a row-gap on one side, a matching padding on the other. The
   legal bar's rule measured 22 below and ZERO above: .footer-grid carries no
   margin-bottom, so it ends on exactly the pixel .footer-bottom's border-top
   starts, and the last column's final line of text sat ON the line.

   MEASURED AT BOTH BREAKPOINTS BEFORE FIXING -- 375px and 1280px each report
   gapAboveLine: 0 -- so this was never the mobile-only defect the screenshot
   suggested. align-items: stretch makes every column that tall, so it was all
   five columns touching the rule on desktop, not one ragged one.

   ONE TOKEN APPLIED TO BOTH SIDES is what stops them drifting apart again: a
   rule and its two neighbours cannot be spaced differently if the space is
   the same declaration. The value is the footer's own vertical gutter at each
   breakpoint, so a rule separates blocks by exactly as much as the layout
   already separates them from each other. */
footer {
  --footer-rule-gap: 32px;
  padding: 56px max(40px, 50% - 650px) 0;
  text-align: left;
  border-top: 1px solid var(--border);
}

/* .footer-top is gone (v7): the two-part top zone became a full-width signup
   band plus a five-column grid, so there is no two-column wrapper any more. */
/* v5 (2026-08-26). Two changes from v4's `1.1fr 1fr 1fr 1fr 1.3fr`:

   The NEWSLETTER LEFT THIS GRID entirely -- it is its own full-width band now
   (.footer-signup, below), which is what frees the 1.3fr it used to hold.

   A SERVICES COLUMN TOOK ITS PLACE, listing the five real /services/ pages.
   That is the SEO half of this pass: those pages previously had one internal
   link each, from the homepage, and none from anywhere else on the site.

   Services gets 1.15fr rather than 1fr because its links are real page titles
   ("Website Design & Development", "AI-Powered Business Solutions") next to
   columns of one- and two-word nav labels; at an even 1fr every one of them
   wrapped to two lines while Explore and Quick Links sat half empty. */
/* Contact as a full-width ROW above the columns (2026-08-26) -- see
   footer.php for why this stopped being a column. Horizontal and wrapping:
   the four items are parallel ways to reach one business, not a list of
   destinations, and as a column they left a ~110px cell beside a Services
   column twice that tall.

   The heading sits INLINE with the items rather than above them, which is
   what makes this read as one tight strip instead of a fourth stacked block.
   It keeps .footer-col-heading's tokens so it still matches the column
   headings underneath, and only overrides the margin they need and it does
   not. */
/* v3 (2026-08-26): a block inside the brand column, not a full-width strip.
   The strip styling that used to live here -- flex, an inline heading, a
   bottom border and 56px of combined padding/margin -- existed to make this
   read as its own band between the brand lockup and the link columns. It is
   not a band any more, so all of it goes: it is the lower half of column one,
   the border would be a stray line across one column, and the heading sits
   above its list the same way every other column heading here does. */
.footer-contact-row {
  display: block;
}

.footer-contact-row-heading {
  margin: 0 0 16px;
}

/* A real GRID, not a wrapping flex row (2026-08-26, per request: "organize
   contact info into a proper table/grid"). Overrides .footer-contact's own
   flex-column stack, which was correct while this was a footer column.

   Wrapped flex items share a left edge only in the FIRST column. Measured at
   375px, the four items fell into a 2x2 whose second column began at x=178
   on row one and x=139 on row two -- each row packs independently, so the
   block looked like a table and behaved like a paragraph. That 39px stagger
   is the whole complaint.

   grid-auto-flow: column keeps the wide layout as one tidy strip of four.
   Below 720px (in the existing phone block near the end of this file) it
   becomes two real columns, each with one shared left edge.
   minmax(0, max-content) rather than plain max-content so a long address
   wraps inside its own column instead of pushing the grid wider than the
   footer -- the items already carry overflow-wrap: anywhere. */
/* Only the margin now (2026-08-26). This used to force a horizontal
   max-content grid because contact was a wide strip; in a column the base
   .footer-contact rule's vertical flex stack is already right, and it is the
   same one-item-per-row rhythm the three link columns beside it use.
   The phone block below re-declares `display: grid` for its own two-column
   arrangement -- it can no longer inherit it from here. */
.footer-contact-row .footer-contact {
  margin: 0;
}

.footer-grid {
  display: grid;
  /* FIVE columns (2026-08-26 v7), matching the reference: brand, Services,
     Explore, Quick Links, Contact. The newsletter is NOT one of them -- it
     is the band above this grid -- and that distinction is what makes five
     columns fine here where v4's five were cramped: v4's fifth was a form
     needing ~340px that got ~130.

     The brand column is not the hollow one it was when it last stood alone
     (295px of width around 88px of content): the lockup STACKS above the
     tagline here rather than sitting beside it, so the column fills.

     Services is the wide one for the reason it always was: its links are
     real page titles ("Website Design & Development", ~200px) beside columns
     of one- and two-word nav labels, and at an even 1fr every one of them
     wrapped while its neighbours sat half empty. Contact is second-widest
     because the email address needs ~162px on one line.

     32px gutter, not 40 -- five tracks need the width back, and the divider
     offsets below are half of whichever number this is. */
  grid-template-columns: 1.15fr 1.25fr 0.85fr 0.85fr 1.1fr;
  gap: 32px;
  /* The space above .footer-bottom's rule. Without it this grid ends on the
     same pixel that border-top begins -- see the token's note at `footer`. */
  margin-bottom: var(--footer-rule-gap);
  /* stretch (grid's own default), not start -- the dividers below are each
     column's own border-left, so they only reach as tall as that column's
     own content. Services is the tallest of the three (five real page
     titles, several of which wrap); stretching the other two to match means
     their dividers run the full row height instead of stopping short.
     Nothing inside any column depends on start-alignment -- they're all
     block-level content that sits at the top of a taller box either way, so
     this is a no-op for everything except the dividers. */
  align-items: stretch;
}

.footer-col {
  min-width: 0;
}

/* Vertical dividers between columns (2026-08-19, per request: "add dividers
   in the footer where it makes sense") -- centered in the grid's own 40px
   gap (margin-left pulls the column 20px into the gap, padding-left pushes
   its content back out past the border), rather than sitting flush at the
   column's own edge with the full 40px on one side and none on the other.
   Brand (:first-child) has nothing to its left, so it's excluded. Reset for
   .footer-col-news on mobile (below) -- it spans the full width there
   (grid-column: 1/-1) with its own border-top already doing this job. */
/* 16px, half the 32px gutter, so each divider sits centred in its gap with
   equal clearance either side. Was -20/20 when the gutter was 40. */
.footer-grid > .footer-col:not(:first-child) {
  border-left: 1px solid var(--border);
  margin-left: -16px;
  padding-left: 16px;
}

/* .footer-col-brand is BACK (2026-08-26 v3) and is no longer the empty column
   that got it removed -- it now holds the lockup AND the contact list, which
   together make it the tallest column in this grid rather than the emptiest.
   No rule of its own is needed: it is a plain block whose two children space
   themselves (see .footer-brand-row's margin-bottom directly below). */
/* Logo + blurb side by side (2026-08-19, per request) -- was stacked, the
   blurb sitting under the logo as its own line. Center-aligned since the
   logo mark (88px, icon+wordmark in one image -- see .footer-logo-img's own
   comment) is much taller than the 2-line blurb text next to it.
   margin-bottom, not a border: the single rule under .footer-contact-row
   already separates this brand+contact pair from the links, and a second
   line 88px above it would cut the identity block in half rather than group
   it. */
/* Stacked, not side by side (v7): as a column among four others there is no
   room for an 88px mark beside a tagline, and stacking is also what fills the
   track -- the emptiness that got this column removed once before came from
   the two sitting in a row with everything under them blank. */
.footer-brand-row {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 14px;
  margin-bottom: 0;
}

.footer-logo {
  display: inline-flex;
  align-items: center;
  flex-shrink: 0;
}

/* 88px, not the old icon-only crop's 56px -- these source files (2026-08-17)
   are the icon+wordmark lockup stacked in one 1250x1250 square (icon ~780px,
   a 124px gap, then the wordmark, per kat-logo-spacing-spec.md), so the icon
   alone renders at roughly the old 56px either way; the extra height is what
   keeps the wordmark itself legible rather than shrinking to a blur. */
.footer-logo-img {
  height: 88px;
  width: auto;
  display: none;
}
.footer-logo-img.footer-logo-dark {
  display: block;
}

[data-theme=light] .footer-logo-img.footer-logo-dark {
  display: none;
}

[data-theme=light] .footer-logo-img.footer-logo-light {
  display: block;
}

.footer-blurb {
  font-size: 0.8125rem;
  color: var(--text-secondary);
  max-width: 300px;
  line-height: 1.6;
  margin: 0;
}

/* Column headings. Same eyebrow treatment the sections use (uppercase,
   tracked, small) but without .eyebrow-kicker's leading dash, which would
   read as a bullet at the top of a list. Doesn't repeat font-family, already
   set on the earlier .footer-col-heading rule (line 439) -- no overlap. */
/* stylelint-disable-next-line no-duplicate-selectors */
.footer-col-heading {
  font-size: var(--label-size);
  font-weight: var(--label-weight);
  text-transform: uppercase;
  letter-spacing: var(--label-tracking);
  color: var(--text);
  margin: 0 0 16px;
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}

.footer-nav-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

/* padding (not just font-size) gives each link a real tap target on touch --
   found at 22px tall with zero hit-area in a mobile audit. The -8px top
   margin cancels the first item's padding against the heading's margin. */
/* Padding bumped 7px -> 11px vertical (2026-08-26, pre-launch accessibility
   audit, RESP-01): at this size the tap target measured 15-16px tall,
   compliant only via WCAG 2.5.8's spacing exception rather than clearing
   24x24 on its own. Costs a little footer height; worth it for a list that
   is mostly tapped on a phone. */
.footer-nav-list a {
  display: inline-block;
  padding: 11px 2px;
  font-size: 0.8125rem;
  color: var(--text-secondary);
}
.footer-nav-list a:hover {
  color: var(--accent-core-text);
}

/* Nudges just margin-top; the earlier margin: 0 above still governs the
   other three sides. -11px, not -7px, since 2026-08-26 (RESP-01) -- this
   cancels the first link's own padding (bumped 7px -> 11px above) against
   the heading's margin, so it MUST move whenever that padding does, or the
   first item in each column drifts away from its heading again. */
/* stylelint-disable-next-line no-duplicate-selectors */
.footer-nav-list {
  margin-top: -11px;
}

/* Real contact details in the footer (2026-08-18, audit finding C-01) --
   phone/email/WhatsApp were previously reachable only by a crawler reading
   the JSON-LD, and since the mobile sticky bar's removal this is the only
   phone link on the site (sms: as of 2026-08-19, tel: before that -- see
   footer.php). Sized a step above .footer-copy: these are meant to be found
   and tapped, not skimmed past like the copyright line.
   Restyled 2026-08-19, per request ("it looks unorganized and busy") -- all
   four rows (phone/email/WhatsApp/location) were previously bold text with
   an underline-style border, all shouting at equal volume with no icon on
   phone/email to help a skim; WhatsApp's icon was the only visual anchor.
   Now every row gets a matching small icon (see footer.php) and only the
   phone number -- the one real phone link, called out above as the thing
   most worth a tap -- stays bold; email/WhatsApp/location step down to the
   same secondary weight/color as everything else in this column, so there's
   one clear primary instead of three competing ones. */
/* v2 (2026-08-19, per request -- "arrange the contact info better, maybe
   columns instead of rows"). Was a single vertical stack of 4 full-width
   rows -- in the ~295px-wide desktop brand column that reads as a long
   list before the visitor even reaches the Explore/Quick Links columns
   next to it. A 2x2 grid paired the two real communication channels on top
   (call/text, email) with the two secondary ones below (WhatsApp,
   location).
   v3 (2026-08-20, per request: moved into its own dedicated footer column,
   see footer.php). Back to a single-column list -- the 2x2 grid was sized
   for the old shared brand column's own ~295px width; this column is
   narrower (~1fr among 5, vs. brand's old 1.5fr among 4) and now sits
   beside Explore/Quick Links, so matching THEIR one-item-per-row rhythm
   reads as a cohesive set of footer columns rather than one column doing
   something visually different from its neighbors. */
.footer-contact {
  display: flex;
  flex-direction: column;
  gap: 10px;
  font-size: 0.8125rem;
}

/* display: block, not inline-flex (2026-08-20 -- root cause of the
   mid-domain email wrap a <wbr> alone didn't fully fix on the narrowest
   3-column mobile row). inline-flex laid the SVG icon and the text out as
   two side-by-side flex items for the item's FULL height, not just its
   first line -- meaning a wrapped second line of text was still squeezed
   into "column width minus icon width minus gap", not the row's full
   width. Confirmed live: at a 107px column, that left only ~85px for
   "kmcspark.com" (needs ~96px), so overflow-wrap:anywhere sliced it again
   even after the <wbr> put the first break in the right place. A plain
   block anchor with the icon inline before the text (vertical-align +
   margin-right, not flex) lets wrapped lines reclaim the icon's width. */
/* padding: 4px 0 added 2026-08-26 (RESP-01) -- these had none at all, so
   the tap target was just the text's own line box, ~15-16px tall at this
   column's font-size. Applied to .footer-contact-loc too even though it
   isn't a link: it's a visual peer in the same list and would otherwise sit
   at a different height than phone/email/WhatsApp beside it. */
.footer-contact a, .footer-contact-loc {
  display: block;
  min-width: 0;
  padding: 4px 0;
  color: var(--text-secondary);
  font-weight: 500;
  text-decoration: none;
  transition: color 0.2s ease;
  overflow-wrap: anywhere;
}

/* user-select: all so the contact details can actually be COPIED (2026-09-07,
   per report: "i cant copy paste any of the phone number or links in kmcspark
   footer"). Reproduced on prod before changing anything: dragging across the
   phone number selected NOTHING, while the same drag across .footer-contact-loc
   -- same column, same rules, but a <span> -- selected "Piscataway, NJ" fine.
   That span is the control that proves the cause is the anchor, not this CSS.

   The mechanism is that an <a href> is drag-first: mousedown-then-move inside
   one is a link drag, so `selectstart` NEVER FIRES and no selection begins.
   Every other list on the page has plain text to start a sweep from and drag
   into the link; this one does not -- phone, email and WhatsApp are each a
   display:block anchor filling its whole row, leaving no non-link pixel to
   press on.

   MEASURED, NOT REASONED -- the two obvious fixes both FAIL here, so don't
   "restore" either one: draggable="false" on the anchor, and
   -webkit-user-drag:none, were each tested alone and together on prod, and
   selectstart still never fired and the selection stayed empty in all three
   cases. user-select:all is what actually worked: one drag (or click-drag of
   any length) selects the WHOLE value, which is what someone copying a phone
   number or an email address wants anyway. Verified in the same session that
   a plain click still fires the anchor's own click with its href intact, so
   tap-to-text and tap-to-mail are untouched.

   Scoped to this list, not `footer a`: these four rows are the copyable
   contact details and the only ones with no adjacent text to grab. The nav
   columns and legal bar keep normal partial selection. */
.footer-contact a, .footer-contact-loc {
  user-select: all;
}

.footer-contact a svg, .footer-contact-loc svg {
  color: var(--text-secondary);
  vertical-align: -2px;
  margin-right: 8px;
}

.footer-contact a:hover {
  color: var(--accent-core-text);
}
.footer-contact a:hover svg {
  color: var(--accent-core-text);
}

/* The one item worth a tap above the rest -- see this block's own comment. */
.footer-contact a:first-child {
  color: var(--text);
  font-weight: 700;
}
.footer-contact a:first-child svg {
  color: var(--text);
}

/* v3 (2026-08-20, per request -- icon-only "doesn't look great," reverted
   to icon+text below; see footer.php's own docblock on this row for the
   full history). WhatsApp's icon is the one exception to every other icon
   in this list running the neutral --text-secondary: #25D366 is WhatsApp's
   own brand green, globally recognized on sight in a way a generic outline
   phone bubble isn't -- using it is what actually answers "maybe use
   favicons," a recognizable brand mark rather than a fourth identical
   line-icon. Contrast computed with the same relative-luminance formula
   the sitewide contrast audit used, not assumed: raw #25D366 is 9.0:1
   against dark mode's --card-bg (#161816) -- green reads as a light color
   against near-black, plenty of margin -- but only 1.98:1 against light
   mode's --card-bg (#FFFFFF), well under WCAG 1.4.11's 3:1 floor for a
   graphical UI object. #1C9E4D (75% of the original channel values) is the
   lightest darkening that clears both light-mode backgrounds this icon can
   sit on (3.47:1 on --card-bg, 3.31:1 on --bg) -- same "smallest nudge that
   clears the floor" approach the sitewide --accent-core-text/--accent-deep
   light-mode fixes already used for the same underlying problem. */
/* .footer-contact a svg.footer-contact-wa-icon, not the bare class -- needs
   to out-specify .footer-contact a svg's own color: var(--text-secondary)
   above (class+type+type beats a single class), since this SVG matches
   that more general descendant selector too. */
.footer-contact a svg.footer-contact-wa-icon {
  color: #25D366;
}

[data-theme=light] .footer-contact a svg.footer-contact-wa-icon {
  color: #1C9E4D;
}

/* Keeps the brand green on hover instead of falling through to
   .footer-contact a:hover svg's --accent-core-text (this icon is
   deliberately not using that shared rule at all). Split by theme, not one
   rule for both -- a single unconditional #25D366 here would silently
   undo the light-mode-safe #1C9E4D above the moment a mouse user hovers
   it, reintroducing the exact 1.98:1 failure that rule exists to fix. */
.footer-contact a:hover .footer-contact-wa-icon {
  color: #25D366;
}

[data-theme=light] .footer-contact a:hover .footer-contact-wa-icon {
  color: #1C9E4D;
}

/* Newsletter (2026-08-22: wired to Zoho Campaigns, see footer.php/
   functions.php). Renders identically to the original 2026-08-18 stub
   whenever newsletter_zoho_enabled is off -- the badge is what stops that
   disabled state reading as a bug; the reduced opacity on the controls is
   what stops it reading as clickable. */
.footer-news-body {
  font-size: 0.8125rem;
  color: var(--text-secondary);
  line-height: 1.6;
  margin: 0 0 16px;
}

.footer-news-badge {
  font-size: 0.5625rem;
  letter-spacing: 0.08em;
  padding: 3px 8px;
  border-radius: 999px;
  border: 1px solid var(--chip-border);
  color: var(--text-secondary);
  font-weight: 700;
}

.footer-news-form {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
}

/* main.js hides the whole form once the subscribe succeeds, leaving only
   .footer-news-status. The display:flex above otherwise beats the browser
   default [hidden] { display: none } -- author stylesheet wins over the UA
   one regardless of order -- so without this rule that hide does nothing.
   Same fix, same reason, as .contact-form[hidden] and .form-success[hidden]. */
.footer-news-form[hidden] {
  display: none;
}

.footer-news-form input {
  flex: 1 1 180px;
  min-width: 0;
  font-family: var(--font-body);
  font-size: 0.875rem;
  padding: 11px 14px;
  border-radius: 8px;
  border: 1px solid var(--border);
  background: var(--card-bg);
  color: var(--text);
}

.footer-news-form input:disabled, .footer-news-form .btn:disabled {
  opacity: 0.55;
  cursor: not-allowed;
}

/* .btn's hover lift would still fire on a disabled button (transform isn't
   gated by :disabled), which is exactly the "this is clickable" signal the
   stub must not send. */
.footer-news-form .btn:disabled:hover {
  transform: none;
  box-shadow: none;
}

.footer-news-form .btn {
  flex: 0 0 auto;
}

/* Honeypot -- kept in the DOM and in normal document flow (not display:none/
   visibility:hidden, which some bots specifically detect and skip filling)
   but visually and functionally invisible: off-screen, zero-size, and out
   of the tab order via the field's own tabindex="-1" in footer.php. */
.footer-news-hp {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

.footer-news-status {
  margin: 10px 0 0;
  font-size: 0.8125rem;
}

/* Same red as .form-submit-error above -- one error color for the whole
   site's forms, not a second one invented for this smaller form. */
.footer-news-status[data-state=error] {
  color: #dc4444;
}

/* --accent-core-text, NOT --accent-core. Brand green #8CD211 measures
   1.85:1 on this white band -- it is a button fill on this site and never
   text (see the auditor email contrast note in CLAUDE.md). That was already
   wrong here and it matters more now: once the form collapses, this line is
   the ONLY thing left in the block. #466709 measures 6.55:1, verified live. */
.footer-news-status[data-state=success] {
  color: var(--accent-core-text);
}

/* The signup band (2026-08-26). Was the fifth column of .footer-grid at
   1.3fr, which on a 1300px page left the email input about 130px wide --
   narrower than most of the addresses anyone would type into it. See
   footer.php for the rest of the reasoning.

   Given a card of its own rather than just more width: it is the one element
   in this footer asking for an action instead of offering a destination, and
   a faint tinted panel is what separates those two jobs without adding a
   second accent colour. --card-bg reads as a lift in dark mode and as a
   panel in light mode, both correct. */
/* v7: the coloured band across the top of the footer -- the one structural
   idea taken from the reference screenshot.

   INLINE, NOT A POPOUT, which was the explicit instruction. The reference
   floats its newsletter panel above the footer so it overlaps the section
   before it; this sits in normal flow, flush with the columns below and
   sharing their left and right edges. No negative margin, no transform, so
   nothing above it has to reserve space.

   GREEN (2026-08-26, per request: "make the newsletter signup in footer bg
   green matching other greens/ctas"). It was near-black for one revision, on
   the reasoning that .cta-banner is a lime gradient sitting directly above
   this footer on every subpage and a second lime slab would repeat it. That
   concern was raised and the call went the other way; the two are separated
   by the footer's own 56px of padding and its top border, and matching the
   site's one CTA colour is worth more than avoiding the echo.

   THE GRADIENT IS .cta-banner's, COPIED EXACTLY -- highlight -> core at 62%,
   not a new one mixed for this block, so there is one green treatment on the
   site rather than two that nearly agree.

   IT MUST NEVER END ON --accent-deep, which is the obvious "more depth" edit
   and the one that breaks it: every stop this gradient touches carries
   --accent-on (#0A0A0A) text, which measures ~11.9:1 on --accent-core and
   ~15.6:1 on --accent-highlight but about 2.4:1 on --accent-deep.

   The tokens are redefined rather than each rule restyled, so the heading,
   body and badge inside adapt on their own. --text-secondary deliberately
   resolves to the SAME near-black as --text here: on lime there is no muted
   grey that stays readable, so hierarchy comes from size and weight instead
   of colour. No opacity, for the reason the legal bar gave it up -- an
   opacity is a colour nobody picked, and it moves when the ground does. */
.footer-signup {
  --text: var(--accent-on);
  --text-secondary: var(--accent-on);
  --card-bg: rgba(255, 255, 255, 0.5);
  --field-bg: #FFF;
  --border: rgba(10, 10, 10, 0.22);
  --accent-core-text: var(--accent-on);
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 0.95fr);
  align-items: center;
  gap: 32px;
  margin: 0 0 44px;
  padding: 30px 34px;
  border-radius: 16px;
  background: linear-gradient(135deg, var(--accent-highlight) 0%, var(--accent-core) 62%);
  color: var(--accent-on);
}

/* A white pill on the green, so the one place to type reads as an opening
   rather than as part of the panel. */
.footer-signup .footer-news-form input[type=email] {
  background: #FFF;
  color: #12160F;
  border-color: transparent;
}
.footer-signup .footer-news-form input[type=email]::placeholder {
  color: #6B7268;
}

/* Near-black on lime, exactly as .cta-banner does it -- the default
   .btn-primary IS lime, which on this ground would be a button-shaped hole.
   Theme-independent on purpose: --accent-on is defined once in :root and
   never overridden, so this pair cannot invert in dark mode. */
.footer-signup .btn-primary {
  background: var(--accent-on);
  color: var(--accent-highlight);
  box-shadow: 0 10px 22px -12px rgba(10, 10, 10, 0.7);
}
.footer-signup .btn-primary:hover {
  box-shadow: 0 14px 26px -12px rgba(10, 10, 10, 0.8);
}

/* The default ring is drawn in var(--bg), which is near-white here and all
   but invisible on lime -- the same trap .cta-banner documents. */
.footer-signup .btn:focus-visible {
  outline-color: var(--accent-on);
}

.footer-signup-copy {
  min-width: 0;
}

/* margin: 0 -- .footer-news-body carries a 16px bottom margin for the old
   stacked column layout, where the form sat underneath it. Side by side,
   that margin is just an off-centre gap. */
.footer-signup-copy .footer-news-body {
  margin-bottom: 0;
}

.footer-signup-form {
  min-width: 0;
}

/* The local line. Prose, so it is set as prose -- the columns above are all
   labels and links, and this being a real sentence is what makes it read as
   a statement about the business rather than as more navigation. */
/* Inside .footer-bottom since 2026-08-26 -- it is the first line of the legal
   bar, not a band of its own. margin-top is gone with the band; the bar's own
   padding-top now does that job. */
.footer-local {
  margin: 0 0 14px;
  font-size: 0.75rem;
  line-height: 1.7;
  color: var(--text-secondary);
  max-width: 68rem;
}
.footer-local a {
  color: var(--text);
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-color: var(--border);
}
.footer-local a:hover {
  color: var(--accent-core-text);
  text-decoration-color: currentcolor;
}

/* ZONE 3: the legal bar, which now carries the local line as well as the
   copyright and the legal nav (2026-08-26).

   NO border-top any more, and that is not an oversight: .footer-signup
   directly above ends with its own border-bottom, so a border here drew a
   second rule a few pixels under the first. It is a block rather than a flex
   row because it has two stacked parts now -- the local sentence, then the
   copyright/nav row -- and that inner row keeps the flex. */
/* The border-top is BACK (2026-08-26 v8). It was dropped when .footer-signup
   sat directly above this and ended with its own border-bottom -- two rules a
   few pixels apart. The signup is the band at the TOP of the footer now, so
   this sits straight under the link columns with nothing separating them, and
   the line is doing real work again. */
.footer-bottom {
  margin-top: 0;
  /* Top padding is the shared token, so the clearance below this rule equals
     the .footer-grid margin above it. The 24px bottom is deliberately NOT
     the token -- that is the page's own bottom edge, not a gap between two
     blocks, and it answers to the footer's padding rather than to a rule. */
  padding: var(--footer-rule-gap) 0 24px;
  border-top: 1px solid var(--border);
  display: block;
}

.footer-bottom-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px 24px;
  flex-wrap: wrap;
}

/* --text-secondary with NO opacity. It used to be this token AT `opacity:
   0.7`, which is a contrast bug waiting for a background change -- it looks
   like a styling choice and behaves like a colour nobody picked, and it did
   fail (4.08:1) the moment this ground briefly went dark. At full strength the
   token measures 5.77:1 on light and 7.46:1 on dark, so it is right in both
   themes and cannot drift. Size, not opacity, is what keeps this quiet. */
.footer-copy {
  font-size: 0.6875rem;
  color: var(--text-secondary);
}

.footer-legal {
  display: flex;
  gap: 18px;
  flex-wrap: wrap;
}

/* display:inline-block + padding added 2026-08-26 (RESP-01) -- a bare
   inline <a> at 11px has no tap target beyond its own text line. */
.footer-legal a {
  display: inline-block;
  padding: 4px 0;
  font-size: 0.6875rem;
  color: var(--text-secondary);
  text-decoration: underline;
  text-underline-offset: 2px;
}
.footer-legal a:hover {
  color: var(--accent-core-text);
}

/* Social icons, matching the shared template-part's .fsocial (2026-09-04) --
   see footer.php for why this footer needed its own copy. 28px square tap
   target (WCAG 2.5.8 -- these are otherwise a bare 15px glyph), same
   --text-secondary/--accent-core-text pairing every other footer icon here
   uses. */
.footer-social {
  display: flex;
  align-items: center;
  gap: 6px;
}

.footer-social a {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  color: var(--text-secondary);
}
.footer-social a:hover {
  color: var(--accent-core-text);
}

/* The mobile sticky CTA bar's styles lived here until 2026-08-18, when the
   bar was removed per request -- see footer.php's note where its markup
   used to be. The mobile `body { padding-bottom: 84px }` that reserved
   space for it went with it. */
/* ---------- Splash screen: "Spark Something" particle intro (v1, restored) ----------
   Restored 2026-08-24 from the delivered mockup (docs/kmcspark/, and the copy
   the client kept). This block had been through three shapes since v1 was
   removed on 2026-08-18 for performance: a bare CSS opacity keyframe with no
   canvas at all (v2), then a rebuilt particle version with a flat-green
   gradient sprite and a 450ms snap into place (v3). v3 was "somewhat close but
   not really there" -- the pacing, the full-viewport convergence and the
   crisp-cored three-tone dots are what the intro actually was, so this is v1's
   own CSS again rather than a fourth interpretation of it.

   The overlay's dismissal is JS-driven here, NOT a CSS @keyframes: main.js's
   phase machine adds .fade-out when the hold ends and sets display:none once
   the transition below has had time to run. v2/v3 drove the fade from CSS
   because their JS had nothing else to do; v1's timeline is a state machine
   and the fade is its last state. Consequence worth knowing: with JS disabled
   nothing would ever remove the overlay, which is why header.php carries a
   <noscript> rule that hides the whole thing outright.

   Duration is the knob that costs PageSpeed points -- not per-frame drawing.
   v1's timeline holds the viewport ~2.95s before this transition starts
   (400ms ambient + 1400ms formation + 900ms hold + 250ms copy fade-out, all
   in main.js's DURATIONS). The v3 it replaces held ~1.3s and measured 90 on
   PSI mobile against a 94 no-splash baseline. If the score needs to come back
   up, shorten DURATIONS.hold rather than the formation. */
#splashScreen {
  position: fixed;
  inset: 0;
  z-index: 500;
  background: #0A0A0A;
  /* Never eat a click meant for the real page underneath. v1 omitted this
     and got away with it because the overlay is gone within ~3.5s, but the
     Skip control is deliberately a sibling rather than a child (see
     header.php) precisely so it stays clickable regardless. */
  pointer-events: none;
  transition: opacity 0.5s ease;
  will-change: opacity;
  /* THE CEILING, and the single thing that bounds how long this opaque
     overlay can cover the page. It is deliberately CSS rather than JS.

     This element is in the HTML and styled by the render-blocking
     stylesheet, so it goes opaque at FIRST PAINT -- but main.js does not run
     until the page has loaded. On a slow connection that gap is seconds, and
     a JS-owned dismiss adds its whole timeline ON TOP of it. Measured on
     PageSpeed mobile (Slow 4G, 4x CPU) when the dismiss was JS-only:
     main.js's first frame at 3.3s, intro still running at 5.5s, ~6s of
     occluded viewport, Speed Index 7.1s, Performance 66.

     Counting from first paint instead makes the worst case a fixed 3s no
     matter how late the script arrives -- and it makes the black-screen
     failure of 2026-08-24 (WordPress silently dropped main.js) impossible by
     construction rather than by a net that main.js had to remember to arm.

     main.js reads this same number and fits its intro inside what is left of
     it; see CEILING_MS there. The two must stay in sync. */
  animation: kmcSplashOut 600ms ease 3000ms forwards;
}

/* main.js dismisses EARLY through this class when its intro finishes ahead of
   the ceiling (or when Skip is pressed). It works because during an
   animation's delay phase no animated value is applied yet, so the normal
   cascade -- and the transition above -- still win. Once the ceiling
   animation is actually running it takes over, and since both end at
   opacity 0 there is no flash either way. */
#splashScreen.fade-out {
  opacity: 0;
  pointer-events: none;
}

@keyframes kmcSplashOut {
  to {
    opacity: 0;
  }
}
/* Only the CSS box -- the canvas's own pixel dimensions are set in JS, at
   min(devicePixelRatio, 2). Unlike v3, which pinned this to 1 on the grounds
   that a pure gradient has no hard edge to sharpen, v1's particles have a
   solid filled core inside their glow, and that core does sharpen. */
#sparkCanvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

/* Set synchronously by header.php's blocking script, before first paint, when
   sessionStorage already shows the splash as seen -- without this, a repeat
   visitor would see one frame of #splashScreen's black background flash
   before main.js's own (necessarily later-running) check could catch up. */
html.kmc-splash-seen #splashScreen, html.kmc-splash-seen .splash-skip {
  display: none;
}

/* Vertical position comes from main.js's layoutTargets(), which knows where
   the mark actually landed and sets --kmc-splash-text-top to the mark's bottom
   plus a proportional gap. It used to be a hardcoded 66% paired with a mark
   pinned to 34% -- two constants nothing kept in agreement, which is exactly
   how a ~175px dead gap opened up between them. Do not put a fixed percentage
   back here; change the gap in layoutTargets() instead.

   The fallback only applies if that JS never ran, in which case header.php's
   <noscript> rule has already hidden the whole overlay anyway. */
#splashText {
  position: absolute;
  left: 50%;
  top: var(--kmc-splash-text-top, 62%);
  transform: translateX(-50%);
  z-index: 2;
  width: 90%;
  max-width: 700px;
  text-align: center;
  pointer-events: none;
}

/* Both fades are driven by the .show class, which main.js adds on the first
   frame and removes when the hold ends -- v1's mechanism. v3 used CSS
   animations with hardcoded delays instead, which meant the copy's timing and
   the particles' timing were two independent sets of constants that had to be
   kept in agreement by hand. */
/* v1 set this to a flat 0.75rem/3px against a headline that clamps to 3.5rem
   -- a 4.7x gap, which reads as a caption under the headline rather than as
   the brand name above it. The clamp below is tuned so the RATIO to
   #splashHeadline stays put at ~0.39x across the whole range rather than just
   happening to look right at one width: both bottom out at the same viewport
   (31.5px/12.4px on a phone), both scale on vw through the middle, and both
   cap within 6px of each other (1145px vs 1151px). Change one and the other
   needs recomputing, or the pair drifts apart at the extremes.

   text-indent compensates for letter-spacing's trailing space: a centred line
   of n characters is laid out n*letter-spacing wide, including a space after
   the LAST glyph, so the visible text sits half a letter-space left of centre.
   Half of 0.22em is the exact correction. Negligible at v1's 3px; visible once
   the tracking is this wide, and this block is being centred against a mark. */
#splashText .eyebrow {
  background: transparent;
  padding: 0;
  border-radius: 0;
  display: block;
  font-size: clamp(0.6875rem, 2.15vw, 1.375rem);
  font-weight: 800;
  /* --eyebrow-tracking (see :root) names this deliberate third tracking
     value; text-indent derives from it via calc so the two can't drift
     apart the way a second hardcoded 0.11em would. */
  letter-spacing: var(--eyebrow-tracking);
  text-indent: calc(var(--eyebrow-tracking) / 2);
  color: var(--accent-core);
  margin-bottom: 14px;
  opacity: 0;
  transition: opacity 0.8s ease;
}

#splashHeadline {
  font-size: clamp(1.75rem, 5.5vw, 3.5rem);
  font-weight: 900;
  letter-spacing: -1px;
  color: #F2F2F2;
  margin: 0;
  opacity: 0;
  transform: translateY(14px);
  transition: opacity 0.9s cubic-bezier(0.16, 1, 0.3, 1), transform 0.9s cubic-bezier(0.16, 1, 0.3, 1);
}

#splashText.show .eyebrow {
  opacity: 1;
}

#splashText.show #splashHeadline {
  opacity: 1;
  transform: translateY(0);
}

/* v1 sat this at bottom/right 30px with no padding, which made the real tap
   target the text glyph itself -- 16x24px, flagged in a mobile audit. The
   inset is reduced and the difference taken as padding instead, so the glyph
   still lands ~30px off both edges while the hit area clears 44px. */
.splash-skip {
  position: fixed;
  bottom: 16px;
  right: 16px;
  padding: 14px 16px;
  background: transparent;
  border: none;
  color: #9AA0A0;
  font-family: var(--font-body);
  font-size: 0.75rem;
  opacity: 0.6;
  cursor: pointer;
  z-index: 501;
  /* Same ceiling, same reason: this control does nothing without JS, and
     nothing else would ever take it off the page.

     Its own keyframe, ending in visibility:hidden, because opacity alone is
     not enough here: unlike #splashScreen this element has no
     pointer-events:none, so at opacity 0 it stays INVISIBLE BUT CLICKABLE --
     a 60x44 fixed target at z-index 501 sitting exactly where the chat
     widget lives. Verified by hit-testing its corners: elementFromPoint
     returned #splashSkip. visibility is what actually takes it out of hit
     testing, and doing it in CSS means it holds even when main.js never runs
     at all -- which is the whole scenario this ceiling exists for. */
  animation: kmcSplashSkipOut 600ms ease 3000ms forwards;
}

@keyframes kmcSplashSkipOut {
  to {
    opacity: 0;
    visibility: hidden;
  }
}
.splash-skip:hover {
  opacity: 1;
}

.splash-skip:focus-visible {
  opacity: 1;
  outline: 2px solid var(--accent-core);
  outline-offset: 2px;
}

/* main.js skips the animation loop entirely under this query -- it draws the
   settled mark once and dismisses. These just stop the copy and the overlay
   from easing on the way out. */
@media (prefers-reduced-motion: reduce) {
  #splashScreen {
    transition-duration: 1ms;
  }
  #splashText .eyebrow,
  #splashHeadline {
    transition-duration: 1ms;
  }
}
/* ---------- Scroll reveal ---------- */
.reveal {
  opacity: 0;
  transform: translateY(28px);
  transition: opacity 0.55s cubic-bezier(0.16, 1, 0.3, 1), transform 0.5s cubic-bezier(0.16, 1, 0.3, 1);
}
.reveal.in-view {
  opacity: 1;
  transform: translateY(0);
}

/* ---------- Scroll progress bar ---------- */
.scroll-progress {
  position: absolute;
  left: 0;
  bottom: 0;
  height: 3px;
  width: 0%;
  background: linear-gradient(90deg, var(--accent-deep), var(--accent-core));
  z-index: 5;
}

/* ---------- Responsive ---------- */
/* Unrelated to the base font-size rule near the top of the file (line 105)
   -- grouped down here with the anchor-scroll rules it's actually paired
   with (section[id]'s scroll-margin-top, immediately below). */
/* stylelint-disable-next-line no-duplicate-selectors */
html {
  scroll-behavior: smooth;
}

section[id] {
  scroll-margin-top: 90px;
}

/* ---------- Industry detail pages (single-industries.php) ----------
   Ported from industries-general-contractors.html / industries-real-estate-
   lawyers.html / industries-cleaning-services.html. --cat-color/--cat-glow
   are set inline per page via the .industry-page wrapper's style attribute
   (single-industries.php) rather than one fixed value here, since the same
   markup/CSS is shared by all three industries with only the accent color
   changing. Recovered here after a botched merge (c5465b7) silently dropped
   this section while keeping single-industries.php itself -- these three
   subpages were shipping with zero styling for every class below. */
/* The homepage Insights teaser (2026-08-26) -- see front-page.php for why
   this is a strip and not the 740px slider it replaces. Rows, not cards: a
   card grid at this depth is another block of boxes competing with the
   sections above it, where a list reads as "here is what is new" and takes a
   third of the height. */
/* Its own padding and heading size, both a step below the sitewide section
   defaults. This is a strip near the bottom of a long page, not one of the
   page's own topics -- at .section's 70px padding and a 2.125rem h2 it
   announced itself louder than the About section above it, which is the
   proportion problem that made the old slider feel expensive as much as its
   raw height did. */
.insights-teaser {
  padding-top: 46px;
  padding-bottom: 46px;
}
.insights-teaser h2 {
  font-size: 1.5rem;
}
.insights-teaser .eyebrow-kicker {
  margin-bottom: 10px;
}

.insights-teaser-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 20px;
  flex-wrap: wrap;
  margin-bottom: 22px;
}
.insights-teaser-head h2 {
  margin: 0;
}

.insights-teaser-all {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  flex: none;
  font-size: 0.8125rem;
  font-weight: 700;
  color: var(--accent-core-text);
}
.insights-teaser-all:hover {
  text-decoration: underline;
  text-underline-offset: 3px;
}

@media (max-width: 620px) {
  .insights-teaser-head {
    align-items: flex-start;
  }
  .insights-teaser {
    padding-top: 38px;
    padding-bottom: 38px;
  }
}
/* Service-area tier headings (2026-08-26). The page now has three tiers
   rather than one flat list -- see page-service-areas.php for why the tiers
   are the honest part. Sized between an h2 and a card title: they divide the
   page, but the h1 above them is the page's actual subject and these must not
   compete with it. */
.service-area-tier-heading {
  font-size: 1.25rem;
  font-weight: 700;
  margin: 44px 0 8px;
}
.service-area-tier-heading:first-of-type {
  margin-top: 34px;
}

.service-area-tier-note {
  font-size: 0.875rem;
  color: var(--text-secondary);
  line-height: 1.65;
  max-width: 62ch;
  margin: 0 0 22px;
}

/* The closing "Don't see your town?" paragraph (2026-08-26, per request:
   "lets fix this spacing too in service areas page").

   It carries .section-lead, whose `margin-top: -14px` exists to TUCK A LEAD
   UNDER ITS HEADING -- every other .section-lead in this theme follows an h1
   or h2, and there the negative pull is right. This one follows a grid of
   town lists, so the pull ran it 14px INTO the last row: "Don't see your
   town?" sat on the line above it with no gap at all.

   44px is not a new number -- it is what .service-area-tier-heading already
   uses after a grid, so this paragraph now sits in the page's own rhythm
   rather than in one invented for it.

   TWO CLASSES, so specificity (0,2,0) beats .section-lead's (0,1,0) wherever
   either rule ends up in the file. A bare .service-area-outro would only win
   by being declared later, which is a rule nobody can see while editing. */
.section-lead.service-area-outro {
  margin-top: 44px;
}

/* TWO columns for the statewide tier, not .about-grid's own auto-fit
   minmax(260px). Those four cells hold 9-22 town names apiece where a county
   cell holds ten, so at 260px they become very tall narrow stacks; at ~580px
   each list reads as a line of prose. 2x2 also avoids the orphan a 3-column
   track leaves when there are four items. */
.service-area-grid-wide {
  grid-template-columns: repeat(auto-fit, minmax(420px, 1fr));
}

/* Breadcrumbs (2026-08-26), replacing .back-link-row and .subpage-back --
   see kmc_spark_breadcrumbs() in functions.php for why one control replaced
   two, and for the BreadcrumbList schema it emits alongside this markup.

   Same gutter formula as .section/.hero/.cta-banner/footer, so the trail
   lines up with the heading underneath it rather than sitting on its own
   margin.

No transition on color (2026-08-20, found live on /free-audit/ while
   auditing the page: clicking the theme toggle left this text stuck at
   its dark-mode gray after switching to light, even though --text-
   secondary itself updates correctly -- confirmed via getComputedStyle,
   the custom property resolves right, the `color` property that
   consumes it via var() just never repaints). Same root cause body's own
   background-color/color transition was already found and fixed for
   (see the `body {}` rule's own comment near the top of this file:
   "confirmed live this causes the theme toggle to get stuck") -- a
   browser-engine quirk where a transitioned property driven
   by a custom property changing on an ANCESTOR, not the element itself,
   can get stuck instead of animating to the new value. That fix's own
   reasoning applies again here: an instant switch beats a visitor stuck
   on the wrong color until they reload. This codebase has 22 other
   `transition: color` rules (2026-08-20 sweep, not fully triaged) --
   worth checking each one for the same failure mode, but only ones whose
   color is actually theme-variable-driven (--text/--text-secondary/etc,
   not a fixed or theme-identical value like --accent-core) are at risk. */
/* Two placements, and this rule only ever suited one of them. Some templates
   print the breadcrumb ABOVE the first <section> (home.php, page-free-audit,
   page-service-areas) and some print it INSIDE it (page-faq, page-about,
   page-why-us, single-services, single, page). Standalone it needs its own
   page gutter and its own top padding, which is what these values are.

   Inside a section it got both twice: the section's own horizontal padding
   plus this margin, and the section's padding-top plus this padding-top --
   while nothing at all separated it from the eyebrow underneath. Measured on
   /faq/ at 375px: the breadcrumb sat at x=40 against an eyebrow at x=20, and
   its bottom edge and the eyebrow's top edge were the same number, 133. That
   is the "doesn't look right".

   .section > .breadcrumbs hands the gutter and the top space back to the
   section and buys one real gap below instead. */
.breadcrumbs {
  margin: 0 max(40px, 50% - 650px);
  padding: 26px 0 0;
  font-size: 0.75rem;
}

.section > .breadcrumbs {
  margin: 0 0 18px;
  padding: 0;
}

.breadcrumb-list {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  list-style: none;
  margin: 0;
  padding: 0;
}

/* The separator is a ::before on every item except the first, rather than a
   character in the markup: a chevron typed into the HTML would be read out by
   a screen reader between every crumb, and generated content in a decorative
   pseudo-element is not announced. */
.breadcrumb-item {
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
}
.breadcrumb-item + .breadcrumb-item::before {
  content: "/";
  color: var(--text-secondary);
  opacity: 0.5;
  flex: none;
}
.breadcrumb-item a {
  color: var(--text-secondary);
  font-weight: 600;
  text-decoration: none;
}
.breadcrumb-item a:hover {
  color: var(--cat-color, var(--accent-core-text));
  text-decoration: underline;
  text-underline-offset: 3px;
}
.breadcrumb-item a:focus-visible {
  outline: 2px solid var(--accent-core);
  outline-offset: 2px;
  border-radius: 3px;
}

/* The current page is the one crumb that is not a link, so it carries the
   weight -- it is a label saying where you are, not a control. */
.breadcrumb-current {
  color: var(--text);
  font-weight: 700;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Wraps .entry-content on single-services.php only (2026-08-19) -- same
   card shell as .included-card below (border/bg/radius/padding), so the
   service description reads as a distinct block on white rather than
   floating on the page's own off-white --bg with nothing to separate it
   from the hero text above.

   margin-top is 0 now: this card is a grid cell of .service-detail (below),
   which owns the spacing that used to sit here. */
.content-card {
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 16px;
  padding: 34px;
  margin-top: 0;
}

/* The 720px cap is what left 465px of the old full-bleed card empty. Inside
   its own grid column the column IS the measure, so the cap only gets in the
   way -- .service-detail's track sizing keeps the line length honest. */
.content-card .entry-content {
  max-width: none;
}

/* Long-form prose pages (Privacy Policy, Terms) — page.php's the_content()
   wrapper. Everything else on this site is bespoke section markup, not
   editor-authored paragraphs/lists, so nothing upstream already styles
   plain block content at this density. */
.entry-content {
  max-width: 720px;
  color: var(--text-secondary);
  line-height: 1.7;
}
.entry-content p {
  margin: 0 0 18px;
}
.entry-content h2 {
  font-size: 1.5rem;
  font-weight: 600;
  color: var(--text);
  margin: 40px 0 14px;
}
.entry-content h2:first-child {
  margin-top: 0;
}
.entry-content ul, .entry-content ol {
  margin: 0 0 18px;
  padding-left: 22px;
}
.entry-content li {
  margin-bottom: 8px;
}
.entry-content a {
  color: var(--accent-core-text);
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* v4 (industries-*_8/_10/_12.html, 2026-08-10): Kat's revision wraps the
   hero in a contained, rounded media panel with a colored ambient
   background behind the text instead of a flat text block. The delivered
   mockup achieves this with a real (heavily compressed, ~5-7KB stock-
   footage-derived) <video>+poster loop per industry -- not shipped here.
   Those clips are just soft drifting glow blobs in the category color, so
   this reproduces the same visual with the aurora-drift technique already
   used below on this same element (radial gradients in --cat-color,
   no binary asset to source/maintain/serve). --cat-rgb/--cat-rgb-dark/
   --cat-on are set inline per page alongside --cat-color/--cat-glow (see
   single-industries.php) for the scrim gradient and the solid-fill eyebrow
   pills' text color. */
.industry-hero {
  position: relative;
  margin: 18px max(40px, 50% - 650px) 20px;
  padding: 0;
  overflow: hidden;
  border-radius: 20px;
  min-height: 440px;
  display: flex;
  align-items: flex-end;
  isolation: isolate;
}

.industry-hero-media {
  position: absolute;
  inset: 0;
  z-index: 0;
  background: #0A0A0A;
  overflow: hidden;
}
.industry-hero-media::before {
  content: "";
  position: absolute;
  inset: -60%;
  background: radial-gradient(circle at 30% 35%, rgba(var(--cat-rgb), 0.55) 0%, transparent 45%), radial-gradient(circle at 72% 60%, rgba(var(--cat-rgb), 0.4) 0%, transparent 50%);
  filter: blur(40px);
  animation: aurora-drift 14s ease-in-out infinite alternate;
  pointer-events: none;
}

@keyframes aurora-drift {
  0% {
    transform: translate(-4%, -4%) rotate(0deg) scale(1);
  }
  50% {
    transform: translate(4%, 5%) rotate(10deg) scale(1.2);
  }
  100% {
    transform: translate(-3%, 3%) rotate(-8deg) scale(1.08);
  }
}
.industry-hero-scrim {
  position: absolute;
  inset: 0;
  z-index: 1;
  background: linear-gradient(180deg, rgba(var(--cat-rgb), 0.14) 0%, rgba(var(--cat-rgb), 0.32) 36%, rgba(var(--cat-rgb-dark), 0.86) 68%, rgba(var(--cat-rgb-dark), 0.96) 100%);
}

[data-theme=light] .industry-hero-scrim {
  background: linear-gradient(180deg, rgba(247, 251, 239, 0.08) 0%, rgba(247, 251, 239, 0.46) 42%, rgba(247, 251, 239, 0.9) 72%, rgba(247, 251, 239, 0.96) 100%);
}

.industry-hero-inner {
  position: relative;
  z-index: 2;
  width: 100%;
  padding: 64px 40px 40px;
}

.industry-hero .tag-label {
  display: inline-block;
  font-size: 0.75rem;
  font-weight: 800;
  letter-spacing: 0.08em;
  color: var(--cat-color);
  text-transform: uppercase;
  margin-bottom: 10px;
  text-shadow: 0 1px 8px rgba(0, 0, 0, 0.4);
}

.industry-hero h1 {
  font-size: 2.625rem;
  font-weight: 700;
  line-height: 1.12;
  max-width: 760px;
  margin-bottom: 16px;
  color: var(--text);
}

.industry-hero .sub {
  font-size: 1rem;
  color: var(--text-secondary);
  max-width: 620px;
  line-height: 1.65;
  margin-bottom: 14px;
}

.industry-hero .who-for {
  font-size: 0.8125rem;
  color: var(--text-secondary);
  margin-bottom: 26px;
}
.industry-hero .who-for strong {
  color: var(--text);
  font-weight: 700;
}

.industry-hero .hero-ctas {
  display: flex;
  gap: 14px;
  flex-wrap: wrap;
}

.industry-hero .btn-primary {
  background: var(--cat-color);
  box-shadow: 0 6px 22px rgba(0, 0, 0, 0.55);
}
.industry-hero .btn-primary:hover {
  box-shadow: 0 0 24px var(--cat-glow);
}

/* CTA banner on industry subpages: button text/icon matches the page's
   category color instead of the sitewide lime accent (.cta-banner
   .btn-primary below stays the homepage default). */
.industry-cta .btn-primary {
  background: var(--bg);
  color: var(--cat-color);
}

/* Light theme swaps in the page's darkened category text variant (--cat-text,
   set inline per page in single-industries.php alongside --cat-color). Raw
   --cat-color as button text on the light --bg (#F7FBEF) measured 1.76:1
   (lime), 2.07:1 (amber), 3.72:1 (blue) and 3.84:1 (purple) -- all under
   WCAG AA's 4.5:1 for this button's 14px bold label. This is the same bug
   --accent-core-text exists to fix for the sitewide lime (see :root above);
   this rule had been overriding that fix on all four industry pages. Dark
   theme is untouched -- the bright category color on #0A0A0A is already
   ~10:1 and is the whole point of the per-page color. */
[data-theme=light] .industry-cta .btn-primary {
  color: var(--cat-text, var(--accent-core-text));
}

/* No max-width here either (was 820px), same reason as .story-card above. */
/* The service-page summary-and-detail pair (2026-08-26 v2 -- see
   single-services.php for the measurements that prompted it).

   0.9fr / 1.1fr rather than an even split: the right panel holds two
   paragraphs and the left holds five short lines, so an even split would put
   the prose on a shorter measure than the list that summarises it. At 1185px
   this gives the prose ~600px of text, about 66 characters -- inside the
   comfortable band -- and the checklist ~430px, enough that the longest item
   (58 characters) sets on two lines at most rather than the ragged mix of one
   and two the old 2-column grid produced.

   align-items: start so the shorter panel keeps its own height instead of
   stretching to match the prose beside it. */
.service-detail {
  display: grid;
  grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr);
  align-items: start;
  gap: 22px;
  margin-top: 40px;
}

@media (max-width: 900px) {
  /* One column below 900px. The two panels each need ~430px of text before
     the checklist starts breaking phrases across three lines; two of them
     plus the gap stops fitting first. Stacked, DOM order puts the summary
     above the detail, which is the reading order this pair was built for. */
  .service-detail {
    grid-template-columns: 1fr;
    gap: 18px;
  }
}
/* A service whose service_features repeater is empty renders no checklist
   panel, so there is no pair to lay out -- see single-services.php. One track
   at a real measure, rather than a lone card in the narrower of two. */
.service-detail-single {
  grid-template-columns: minmax(0, 760px);
}

/* The checklist panel. It DOES have a card of its own now, unlike the flat
   list this replaced -- what made a card wrong before was that it sat
   directly above another card in a single column, reading as two competing
   panels. Side by side they read as a pair, which is the point. */
.service-included {
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 16px;
  padding: 28px;
}

/* Two selectors, not one: this is an <h2> inside .section, and
   `.section h1, .section h2` is class+type -- a lone class loses to it and
   the label would render at 2.125rem. Exactly the bug that had every
   /insights/ card title rendering as a section heading; see
   .insight-card .insight-card-title. */
.service-included .service-included-label {
  font-size: var(--label-size);
  font-weight: var(--label-weight);
  letter-spacing: var(--label-tracking);
  text-transform: uppercase;
  color: var(--text-secondary);
  margin: 0 0 16px;
}

.service-checklist {
  list-style: none;
  display: grid;
  grid-template-columns: 1fr;
  gap: 12px;
  margin: 0;
  padding: 0;
}
.service-checklist li {
  position: relative;
  padding-left: 26px;
  font-size: 0.875rem;
  font-weight: 600;
  line-height: 1.5;
  color: var(--text);
}

/* Same tick and the same light-theme contrast swap as .included-card below --
   these marks carry meaning (they say what is included), so they need WCAG
   1.4.11's 3:1 non-text floor, which raw --cat-color misses in light mode. */
.service-checklist li::before {
  content: "✓";
  position: absolute;
  left: 0;
  top: 0;
  font-weight: 800;
  color: var(--cat-color, var(--accent-core-text));
}

[data-theme=light] .service-checklist li::before {
  color: var(--cat-text, var(--accent-core-text));
}

/* Deliberately two selectors (0,2,0) rather than a lone class. .eyebrow
   itself is a single class (0,1,0), so a lone .industry-included-eyebrow
   would only win by being declared LATER in this file -- a rule whose
   correctness depends on source order is one nobody can see while editing,
   and this file is edited by concurrent sessions. Scoping it under .section
   makes it win on specificity instead, order-independently. Same family of
   trap as the .insight-card-title / .diff-card h3 comments in this file. */
.section .industry-included-eyebrow {
  background: var(--cat-color);
  color: var(--cat-on);
  font-size: 0.8125rem;
  padding: 8px 18px;
}

.included-card {
  border: 1px solid var(--border);
  background: var(--card-bg);
  border-radius: 16px;
  padding: 34px;
}
.included-card ul {
  list-style: none;
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 14px 24px;
  margin-top: 6px;
}
.included-card li {
  font-size: 0.875rem;
  color: var(--text-secondary);
  padding-left: 22px;
  position: relative;
  line-height: 1.5;
}
.included-card li::before {
  content: "✓";
  position: absolute;
  left: 0;
  color: var(--cat-color, var(--accent-core-text));
  font-weight: 800;
}

/* Same light-theme contrast swap as .industry-cta above -- these check marks
   carry meaning (they mark what is included), so they need the 3:1 non-text
   contrast floor, which raw --cat-color misses in light theme. */
[data-theme=light] .included-card li::before {
  color: var(--cat-text, var(--accent-core-text));
}

/* ============================================================
   Insights (blog) -- index cards + single article
   ------------------------------------------------------------
   Every color goes through the same custom properties the rest of
   this file uses, so light/dark parity is automatic. Note
   --accent-core-text (NOT --accent-core) for anything that renders
   as text: the raw brand lime computes to ~1.7:1 on the light
   theme's --bg, which is why the light block redefines it (see the
   comment at :root).
   ============================================================ */
.insights-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
  gap: 20px;
  margin-top: 34px;
}

.insight-card {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 26px 24px 22px;
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 16px;
  transition: border-color 0.25s ease, transform 0.25s ease;
}
.insight-card:hover {
  border-color: var(--accent-core);
  transform: translateY(-3px);
}

/* The sitewide label token, not a sixth hand-rolled near-copy of it (see
   --label-* in :root). This was 0.75rem/400/0.06em next to
   .work-card-industry's 0.75rem/800/0.12em doing the identical job. */
.insight-card-meta {
  font-size: var(--label-size);
  font-weight: var(--label-weight);
  letter-spacing: var(--label-tracking);
  text-transform: uppercase;
  color: var(--text-secondary);
}

/* `.insight-card .insight-card-title`, NOT the bare class. `.section h1,
   .section h2` (~line 1068) is class+type and out-specifies a lone class, so
   for as long as this rule was written the other way EVERY card title on
   /insights/ rendered at 2.125rem/38.25px -- a full section heading, 79%
   oversize, four lines deep on a 332px card, and by a wide margin the
   loudest thing on the page. Measured rather than guessed: getComputedStyle
   returned 38.25px while this rule declares 1.1875rem, and the cascade shows
   both rules matching the element. The same trap is recorded against
   .diff-card h3 further down this file, so this is its second outing here --
   any card-level heading that is an <h2> or <h1> inside .section needs two
   selectors, not one.

   600, not 700: the settled convention is display face at 600 for card rank
   with 700 reserved for h1-h2 (.service-name and .accordion-title strong are
   both 600 doing exactly this job). At 700 and section size together, this
   was heavier than the page's own <h1>.

   -0.2px was leftover sans tracking, same as .insight-single h1 above — not
   caught by the original sweep since this rule has no font-family
   declaration of its own (it's an <h2>, inherits Bitter from the base
   heading rule) so it never matched a search for explicit font-family. */
.insight-card .insight-card-title {
  font-size: 1.1875rem;
  font-weight: 600;
  line-height: 1.35;
  margin: 0;
}

.insight-card-title a {
  color: var(--text);
  text-decoration: none;
}
.insight-card-title a:hover {
  color: var(--accent-core-text);
}

.insight-card-excerpt {
  font-size: 0.9375rem;
  line-height: 1.6;
  color: var(--text-secondary);
  margin: 0;
  flex-grow: 1;
}

.insight-card-link {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin-top: 4px;
  font-size: 0.875rem;
  font-weight: 600;
  color: var(--accent-core-text);
  text-decoration: none;
}
.insight-card-link:hover {
  gap: 10px;
}
.insight-card-link svg {
  transition: transform 0.2s ease;
}

.insights-empty {
  color: var(--text-secondary);
  margin-top: 30px;
}

/* --- Insights filter row + card category ---
   Chips, but every one of them is an <a> to a real category archive at
   /insights/topics/<slug>/ -- see kmc_spark_insights_filters() in functions.php
   for why that is the requirement rather than a preference.

   `.insights-filter`, not a bare `a`: this row sits inside `.section`, which
   already styles descendant links, and a rule that depended on being later in
   the file would be invisible to whoever edits it next. Same trap the
   .insight-card-title comment above records. */
.insights-filters {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 28px 0 6px;
}

.insights-filter {
  display: inline-flex;
  align-items: center;
  padding: 6px 14px;
  border: 1px solid var(--border);
  border-radius: 999px;
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--text-secondary);
  text-decoration: none;
}
.insights-filter:hover {
  color: var(--accent-core-text);
  border-color: var(--accent-core);
}

/* The current filter is a link to the page you are already on. It stays a
   link (removing it would reflow the row on every navigation) but it is
   marked aria-current="page" in the markup, and this is the visual half of
   that. */
.insights-filter.is-current {
  background: var(--accent-core);
  border-color: var(--accent-core);
  /* --accent-on is this palette's "text that sits on the accent fill"
     (#0A0A0A), the same pairing .cta-banner .btn-primary and
     .footer-signup .btn-primary already use. Lime with white text on it
     fails contrast outright. */
  color: var(--accent-on);
}

.insight-card-dot {
  color: var(--text-secondary);
}

.insight-card-category {
  color: var(--accent-core-text);
  text-decoration: none;
  font-weight: 600;
}
.insight-card-category:hover {
  text-decoration: underline;
}

.insights-feed {
  margin-top: 22px;
  font-size: 0.8125rem;
  color: var(--text-secondary);
}
.insights-feed a {
  color: var(--text-secondary);
  text-decoration: none;
}
.insights-feed a:hover {
  color: var(--accent-core-text);
  text-decoration: underline;
}

/* --- Single article --- */
.insight-single {
  max-width: 760px;
}

.insight-single-meta {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 0.8125rem;
  color: var(--text-secondary);
  margin-bottom: 14px;
}

.insight-single h1 {
  font-size: 2.125rem;
  line-height: 1.2;
  /* Explicitly "normal", not just omitted (2026-08-19 audit fix) — this was
     -0.6px, left over from the old sans. Omitting the property here isn't
     enough: the bare `h1` rule sets -0.5px globally (that's the homepage
     hero's own tracking), and this selector doesn't outrank it on
     specificity by staying silent — it has to actually restate normal, or
     a plain blog-post title inherits the giant hero headline's tracking. */
  letter-spacing: normal;
  margin-bottom: 26px;
}

.insight-single-body {
  max-width: none;
}
.insight-single-body h3 {
  font-size: 1.125rem;
  font-weight: 700;
  color: var(--text);
  margin: 30px 0 12px;
}
.insight-single-body blockquote {
  margin: 24px 0;
  padding: 4px 0 4px 20px;
  border-left: 3px solid var(--accent-core);
  color: var(--text);
  font-style: italic;
}

/* Sources block -- rendered by kmc_blog_render_sources() in the shared
   theme's inc/blog-ingest.php, so these class names are shared-theme
   contract, not kmcspark-local naming. */
.kmc-blog-sources {
  margin-top: 44px;
  padding: 24px 22px;
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 14px;
}

.kmc-blog-sources-title {
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-secondary);
  margin: 0 0 14px;
}

.kmc-blog-sources-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

.kmc-blog-source {
  margin-bottom: 12px;
  line-height: 1.5;
}
.kmc-blog-source:last-child {
  margin-bottom: 0;
}
.kmc-blog-source a {
  color: var(--accent-core-text);
  font-size: 0.9375rem;
  text-decoration: underline;
  text-underline-offset: 2px;
}

.kmc-blog-source-meta {
  display: block;
  font-size: 0.8125rem;
  color: var(--text-secondary);
  margin-top: 2px;
}

.insight-single-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 28px;
}

.insight-single-cta {
  margin-top: 48px;
  padding: 32px 28px;
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 18px;
  text-align: center;
}
.insight-single-cta h2 {
  font-size: 1.375rem;
  font-weight: 600;
  margin: 0 0 8px;
}
.insight-single-cta p {
  color: var(--text-secondary);
  font-size: 0.9375rem;
  margin: 0 0 20px;
}

/* The reassurance note sits AFTER the button here, so the rule above would
   otherwise give it a 20px trailing margin and body-copy sizing (that
   selector outranks the bare .cta-banner-note class). */
.insight-single-cta .cta-banner-note {
  margin: 12px 0 0;
  font-size: 0.75rem;
}

/* Core's the_posts_pagination() markup */
.pagination {
  margin-top: 38px;
}
.pagination .nav-links {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
.pagination .page-numbers {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 38px;
  height: 38px;
  padding: 0 12px;
  border: 1px solid var(--border);
  border-radius: 10px;
  color: var(--text-secondary);
  font-size: 0.875rem;
  text-decoration: none;
}
.pagination .page-numbers:hover {
  border-color: var(--accent-core);
  color: var(--text);
}
.pagination .page-numbers.current {
  background: var(--accent-core);
  border-color: var(--accent-core);
  color: #0A0A0A;
  font-weight: 700;
}

@media (max-width: 720px) {
  .industry-hero {
    min-height: 360px;
    border-radius: 16px;
  }
  .industry-hero-inner {
    padding: 40px 22px 28px;
  }
  .industry-hero h1 {
    font-size: 1.875rem;
  }
  /* .service-checklist is unconditionally one column now (see its own rule)
     and no longer belongs in this list. */
  .included-card ul {
    grid-template-columns: 1fr;
  }
  .specialty-grid {
    grid-template-columns: 1fr;
  }
  /* One column below 720px -- two 1fr columns at phone width leave each
     contrast row too narrow for the body line to hold together. Padding and
     gap are tightened with it: four stacked rows is where this section's
     whole vertical budget goes, and the 2026-08-24 pass that emptied this
     section did it because 762px here was too much. */
  .why-us-teaser-grid {
    grid-template-columns: 1fr;
    gap: 12px;
  }
  .why-us-teaser-card {
    padding: 14px 16px;
  }
  .why-us-teaser-card h3 {
    margin-bottom: 7px;
  }
  /* The card's top-right corner is not a viable home for the form chrome at
     phone widths -- the card is only ~335px wide and the heading wraps to
     two or three lines, so eight dots plus a 38px control would collide with
     it. Both return to the flow here. The toggle sits back on the heading
     row (its original layout, which was only ever wrong on a WIDE card), and
     the dots sit above the fields with real spacing rather than the -4px
     that made them look wedged in. */
  /* THE TOGGLE STAYS ABSOLUTE HERE TOO (2026-08-26, per request: "remove
     this empty space pls"). It used to drop back into the flow at this
     width, which cost 84px above the first field for a 38px button and a
     9px row of dots: the button's own line, then .contact-layout's 32px
     flex gap, then the dots. Out of flow it contributes no height at all,
     .contact-intro-row collapses to zero on its own (no display:none
     needed, and none WANTED -- that would hide the button with it), and the
     32px gap has nothing left to separate.

     That was the right call while the row held the intro paragraph, which
     the button sat beside. With the paragraph gone it was a whole line
     spent on one control.

     What has to move with it is .form-progress's bottom margin: the dots
     are only ~100px of the row's width so they clear the button (measured:
     dots end at x=140, the button starts at x=296), but the first FIELD is
     full width and would run under it.

     12px, not the 26 it looks like it should be: .contact-form is a flex
     column with gap: 16px, and in flexbox a margin and a gap ADD. 12 + 16
     puts the first field 9px below the button's 58px reach (20px inset +
     38px control). Those three numbers are one measurement -- change the
     button's size or inset, or the form's gap, and this has to follow. */
  .form-progress {
    position: static;
    margin: 4px 0 12px;
  }
  /* The card's 34px desktop padding is a third of a 375px viewport once both
     sides are counted, which would squeeze the form controls it exists to
     frame. The card still reads as a card at 20px. */
  /* gap: 0 -- .contact-info holds only the now-absolute sound toggle, so it
     is a zero-height flex child and the 32px gap was 32px between nothing
     and the form. See the .form-progress note above. */
  .contact-layout {
    padding: 26px 20px 24px;
    border-radius: 14px;
    gap: 0;
  }
  /* The rules that used to sit here hid .back-link-text on phones, leaving a
     lone 20px house icon floating above the page -- which is the thing the
     2026-08-26 request called "straggling". A breadcrumb cannot degrade that
     way: it is text, so it wraps instead of disappearing, and the current
     crumb ellipsises rather than pushing the row wide. */
  .breadcrumbs {
    padding-top: 20px;
    font-size: 0.6875rem;
  }
  /* ONE VERTICAL RHYTHM FOR THE WHOLE FOOTER (2026-08-26, per request:
     "make the gaps/spacing consistent/logical here"). Measured text-to-text
     before the fix, the link columns ran heading -> 16 -> link -> 16 -> link
     while the contact block eight pixels above them ran heading -> 26 ->
     item -> 10 -> item. Three different numbers doing one job.

     16px everywhere now, so the contact strip reads as a fourth column of
     the same table rather than a differently-spaced block that happens to
     sit above it.

     display: block is what makes the heading's 16 reachable. Inline with the
     items it is a wrapped flex line, and a flex row-gap sits on top of the
     line box's own leading -- 10px of gap measured 26px of visible space.
     The desktop rule stays flex: there the heading really is inline with the
     items, which is what makes that layout one tight strip. */
  /* No bottom padding/margin here any more (2026-08-26 v3): this is the
     lower half of the brand column, and the grid's own 26px row-gap already
     separates that column from Services/Explore beneath it. Left in, the two
     added up to 52px. */
  .footer-contact-row {
    display: block;
  }
  .footer-contact-row-heading {
    margin-bottom: 16px;
  }
  /* Two real columns instead of the wide one-row strip -- see this rule's
     own comment above for the ragged second column this replaced.
     The template is .footer-grid's, deliberately: these two grids sit one
     above the other and a reader reads them as one table, so they share a
     column axis. max-content sized this to its own contents and put the
     second column 22px left of Explore's. */
  .footer-contact-row .footer-contact {
    /* Declared here now (2026-08-26): the desktop rule this used to
       inherit `display: grid` from is a plain vertical stack since the
       contact list became part of the brand column. Without this the
       grid-* properties below would be inert against a flex column and
       the four items would stack in one tall row. */
    display: grid;
    row-gap: 16px;
    /* COLUMN flow, so the email lands in the WIDE track. Row flow read
       phone, email / WhatsApp, location -- which put the one item that
       needs 162px into the 151px column and wrapped it, while the 166px
       column held a phone number that needs 110. Filling down first gives
       phone, email down the left and WhatsApp, location down the right,
       and every cell is one line. */
    grid-auto-flow: column;
    grid-template-rows: repeat(2, auto);
    grid-template-columns: 1.3fr 1fr;
    column-gap: var(--f-gap);
    /* This is a flex ITEM of .footer-contact-row, so without a width it
       sizes to its own contents -- 327px against the link grid's 335,
       which moved its second column 4px left of Explore's and undid the
       shared axis this rule exists for. */
    width: 100%;
  }
  .insights-grid {
    grid-template-columns: 1fr;
    gap: 16px;
  }
  .insight-single h1 {
    font-size: 1.75rem;
  }
  .insight-single-cta {
    padding: 26px 20px;
  }
}
/* =====================================================================
   First-impression + subpage system pass (2026-08-24, per request:
   "im not happy with how this looks ... especially the hero/above the
   fold first impression", whole-site, fix-and-elevate within brand).

   Appended at the END of the file on purpose. This stylesheet is
   desktop-first with max-width overrides, and several of those override
   blocks (the 900px readability pass, the 480px hero pass) sit ABOVE
   this point. Anything here that needs to survive at phone widths
   therefore carries its own max-width block further down in this same
   section, so source order -- not specificity games -- settles it.
   ===================================================================== */
/* ---------- Hero: measured text column + "what you get" brief ----------
   The hero was a single full-bleed text column: at 1280px the H1 ran the
   full 1185px and the lead paragraph rendered as ONE 1185px line, which
   is roughly 160 characters -- two to three times a readable measure. The
   old comment above .hero-content argued a max-width would "leave dead
   space on the right", and that was true as far as it went; the answer is
   to put something in that space rather than to let prose run to the
   window edge.

   What goes there is a plain-language summary of what this business
   actually sells, for someone who has never heard of it. That REPLACES a
   live-client-work panel that briefly occupied this column earlier in the
   same pass; it was removed per request ("i dont think the portfolio on
   top like that is visually/logically correct"). See the hero markup in
   front-page.php for why that objection is correct and where portfolio
   proof belongs instead.

   Consequences of the swap worth knowing before touching this:
     - The panel is now pure text, so it costs no image fetch and RENDERS
       ON MOBILE too. The old display:none + loading="lazy" pairing that
       kept a hidden screenshot off phones is gone with it, because there
       is no longer an image to keep off them.
     - It is unconditional -- there is no content it can fail to find --
       so .has-brief is emitted always. The gate class is kept anyway so
       .hero stays single-column for any other template that uses it. */
.hero.has-brief {
  display: grid;
  grid-template-columns: minmax(0, 1.12fr) minmax(0, 0.88fr);
  gap: 56px;
  align-items: center;
}

/* Measure caps, applied whether or not the proof panel renders -- with the
   panel the grid column already narrows the text and these are a backstop
   for very wide monitors; without it they are the whole fix, since the
   text would otherwise run the full 1185px content box again. */
.hero .hero-content h1 {
  max-width: 16ch;
}

.hero .hero-content p.sub {
  max-width: 54ch;
}

/* The brief panel itself. Same card chrome as the rest of the page
   (border + --card-bg + 16px radius) so it reads as part of the system
   rather than a hero-only flourish. */
.hero-brief {
  min-width: 0;
  border: 1px solid var(--border);
  background: var(--card-bg);
  border-radius: 16px;
  padding: 26px 28px;
}

.hero-brief-title {
  font-family: var(--font-body);
  font-size: var(--label-size);
  font-weight: var(--label-weight);
  letter-spacing: var(--label-tracking);
  text-transform: uppercase;
  color: var(--accent-core-text);
  margin-bottom: 18px;
}

.hero-brief-list {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 16px;
  margin: 0;
  padding: 0;
}
.hero-brief-list li {
  position: relative;
  padding-left: 26px;
}

/* Check mark rather than a bullet: these are things you receive, not
   items in a list of topics. --accent-core-text, never raw --accent-core
   -- the brand lime measures about 1.7:1 on the light theme's background
   and fails WCAG outright as text or as a glyph this small. */
.hero-brief-list li::before {
  content: "✓";
  position: absolute;
  left: 0;
  top: 0;
  color: var(--accent-core-text);
  font-weight: 800;
  font-size: 0.875rem;
  line-height: 1.5;
}

.hero-brief-list strong {
  display: block;
  font-family: var(--font-display);
  font-size: 1rem;
  font-weight: 700;
  line-height: 1.35;
  margin-bottom: 2px;
}

.hero-brief-list span {
  display: block;
  font-size: 0.8125rem;
  color: var(--text-secondary);
  line-height: 1.5;
}

/* ---------- Google review strip (.review-strip) ----------
   A slim band between the hero and #services, NOT a .section. That is
   deliberate: the 2026-08-24 shortening pass cut this homepage from ~13
   screens to ~9, and a full .section here would spend 140px of padding plus
   an eyebrow and an h2 to deliver one number. This says the same thing in
   one row, and adds no heading to the page outline.

   Every colour here is a token, so the strip follows the theme toggle with
   no dark-mode rules of its own. The one literal is Google's own blue on
   the G mark, which is a fixed brand colour in both modes and is aria-hidden
   decoration sitting beside the word "Google" in the text below it. */
.review-strip {
  margin: 10px max(40px, 50% - 650px) 0;
}

.review-strip-inner {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 8px 16px;
  padding: 16px 24px;
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 14px;
}

.review-strip-mark {
  display: inline-flex;
  color: #4285f4;
}

/* Display face for the numeral, matching the convention the --font-display
   comment sets out: big stat numerals go through the serif, because that is
   where matching the wordmark reads. 700 rather than the card-rank 600 --
   this is the loudest thing in the strip and there is no h1/h2 here for it
   to compete with. */
.review-strip-score {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 1.75rem;
  line-height: 1;
  color: var(--text);
}

/* Two stacked rows of the same five stars. The track sizes the box and
   renders the empty state; the fill is laid over it and clipped to
   --review-star-fill (set inline by front-page.php from the real rating),
   so a fractional rating renders as a partial star instead of being
   rounded into a claim nobody made. */
.review-stars {
  position: relative;
  display: inline-flex;
  line-height: 0;
  color: var(--border);
}

.review-stars-track {
  display: inline-flex;
}

.review-stars-fill {
  position: absolute;
  inset: 0 auto 0 0;
  display: inline-flex;
  width: var(--review-star-fill, 100%);
  overflow: hidden;
  /* NOT --accent-core. That is #8CD211, which measures ~1.7:1 against the
     light theme's --card-bg -- below the 3:1 WCAG floor for a meaningful
     graphic, and these stars ARE the rating, not decoration.
     --accent-core-text is the already-vetted darker value in light mode
     and the raw lime in dark. Same reasoning as .eyebrow-kicker's fix. */
  color: var(--accent-core-text);
}

/* Without this the svgs are flex items inside a percentage-width, overflow
   hidden box, so they would SHRINK to fit rather than being clipped -- five
   narrower stars instead of four and a bit. */
.review-stars svg {
  flex: none;
}

.review-strip-meta {
  margin: 0;
  font-size: 0.9375rem;
  color: var(--text-secondary);
}

.review-strip-link {
  font-size: 0.9375rem;
  font-weight: 600;
  color: var(--accent-core-text);
  text-decoration: underline;
  text-underline-offset: 3px;
}
.review-strip-link:hover, .review-strip-link:focus-visible {
  text-decoration-thickness: 2px;
}

@media (max-width: 560px) {
  .review-strip {
    margin-left: 20px;
    margin-right: 20px;
  }
  .review-strip-inner {
    padding: 14px 16px;
    gap: 6px 12px;
  }
  /* The link drops to its own full-width line below the rating rather than
     trying to sit beside it -- at this width it otherwise wraps into a
     ragged second row that reads as a layout bug. */
  .review-strip-link {
    flex-basis: 100%;
    text-align: center;
  }
}
/* ---------- Subpage hero (.hero-sub) ----------
   .hero-sub had NO rule in this stylesheet at all. It is a
   _shared/kmc-base-theme class, and kmcspark dequeues that stylesheet
   (kmc_spark_dequeue_shared_theme_assets()), so every subpage hero was
   rendering as a bare .section -- same 70px padding as a mid-page block,
   no distinction between "top of a page" and "another section". */
.hero-sub {
  padding: 44px 0 8px;
  border-bottom: 1px solid var(--border);
  margin-bottom: 8px;
}
.hero-sub h1 {
  max-width: 20ch;
  margin-bottom: 18px;
}

/* .why-us-lede is normalised alongside .section-lead so the three subpage
   heroes share one rhythm. page-why-us.php keeps its own lede class rather
   than switching to .section-lead because the accent left border is
   carrying real meaning there (that line is the page's thesis, the cards
   below are its evidence) — but its stock -14px top margin was tuned for a
   mid-page section, not for sitting under a page H1. */
.hero-sub .section-lead,
.hero-sub .why-us-lede {
  max-width: 62ch;
  margin-top: 0;
  margin-bottom: 26px;
}

/* Back-to-home affordance. kmcspark's nav is a hamburger slide-out built
   for a one-pager, so a visitor who lands on a subpage from search has no
   visible route back to the funnel without opening a menu. */
/* .subpage-back's rules lived here until 2026-08-26. It was the second of
   two back-control treatments (see .breadcrumbs above); both are gone. */
/* ---------- Why Us page grid ----------
   page-why-us.php originally reused .diff-grid/.diff-card. Two things
   went wrong with that, both visible:

   1. main.js's "Why Us carousel" block guards on cards.length < 2, not
      on the presence of dots -- so it DID wire this grid, and
      enableScrollAffordance() put .is-scrollable on it. With no
      .is-at-start/.is-at-end companion state the both-edges mask rule
      won, fading the first and last cards' edges on a grid that does not
      scroll and has no dots to drive it.
   2. The cards used <h2>, but the card styling is written against
      .diff-card h3 -- so ".section h1, .section h2" won instead at
      2.125rem with a 30px bottom margin, blowing the cards past their
      own grid box (scrollHeight 314 vs clientHeight 286).

   Own classes here rather than a fix to .diff-*: that component is the
   homepage's mobile carousel and should keep behaving exactly as it
   does. This page wants all four cards visible at once, which is what
   its own docblock always said it wanted. */
.why-us-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 24px;
}

.why-us-card {
  border: 1px solid var(--border);
  background: var(--card-bg);
  border-radius: 16px;
  padding: 26px;
  transition: border-color 0.3s ease, box-shadow 0.3s ease, transform 0.3s ease;
}
.why-us-card:hover {
  border-color: var(--accent-core);
  box-shadow: 0 14px 34px -18px rgba(0, 0, 0, 0.4);
  transform: translateY(-3px);
}
.why-us-card h3 {
  font-family: var(--font-display);
  font-size: 1.125rem;
  font-weight: 600;
  margin-bottom: 8px;
}
.why-us-card > p {
  font-size: 0.8125rem;
  color: var(--text-secondary);
  line-height: 1.6;
  margin-bottom: 16px;
}
.why-us-card ul {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.why-us-card li {
  font-size: 0.8125rem;
  color: var(--text);
  padding-left: 22px;
  position: relative;
  line-height: 1.5;
}
.why-us-card li::before {
  content: "✓";
  position: absolute;
  left: 0;
  top: 0;
  color: var(--accent-core-text);
  font-weight: 800;
}

/* ---------- Portfolio page (2026-08-27) ----------
   Deliberately its own class pair, not .work-grid/.work-card -- see
   page-portfolio.php's docblock: main.js selects .work-grid unconditionally
   to wire the homepage's carousel, and this page wants every case study
   visible at once with no carousel at all. Grid/card chrome copied from
   .why-us-grid/.why-us-card (same "show everything, no swipe" page), media
   handling copied from .work-card-media (same aspect-ratio/object-fit a
   case-study photo needs); .work-card-badge/.work-card-industry are reused
   directly since those are plain chip styles with no grid/carousel coupling
   of their own. */
.portfolio-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 24px;
}

.portfolio-card {
  border: 1px solid var(--border);
  background: var(--card-bg);
  border-radius: 16px;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  transition: border-color 0.3s ease, box-shadow 0.3s ease, transform 0.3s ease;
}
.portfolio-card:hover {
  border-color: var(--accent-core);
  box-shadow: 0 14px 34px -18px rgba(0, 0, 0, 0.4);
  transform: translateY(-3px);
}

.portfolio-card-media {
  aspect-ratio: 16/10;
  overflow: hidden;
  background: var(--bg-2);
}
.portfolio-card-media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: top;
  display: block;
  transition: transform 0.4s ease;
}

.portfolio-card:hover .portfolio-card-media img {
  transform: scale(1.04);
}

.portfolio-card-body {
  padding: 20px 22px 24px;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 8px;
}
.portfolio-card-body h3 {
  font-family: var(--font-display);
  font-size: 1.125rem;
  font-weight: 600;
  margin-top: 2px;
}
.portfolio-card-body p {
  font-size: 0.8125rem;
  color: var(--text-secondary);
  line-height: 1.6;
}

/* ---------- Homepage "Our Work" teaser (2026-08-27) ----------
   Replaces the old homepage carousel: one representative card (no dots, no
   scroll) plus the link to /portfolio/ for the rest. Media/badge/industry
   reuse the same .work-card-media/.work-card-badge/.work-card-industry
   chips as before for visual continuity with the full page; the card itself
   is a plain flex row rather than either grid, since there is exactly one
   of it. */
.work-teaser {
  display: flex;
  gap: 24px;
  align-items: stretch;
  border: 1px solid var(--border);
  background: var(--card-bg);
  border-radius: 16px;
  overflow: hidden;
}

.work-teaser-media {
  flex: 1 1 40%;
  max-width: 360px;
  aspect-ratio: 16/10;
  overflow: hidden;
  background: var(--bg-2);
}
.work-teaser-media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: top;
  display: block;
}

.work-teaser-body {
  flex: 1 1 60%;
  padding: 26px 28px;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 10px;
  justify-content: center;
}
.work-teaser-body h3 {
  font-family: var(--font-display);
  font-size: 1.25rem;
  font-weight: 600;
  margin-top: 4px;
}
.work-teaser-body p {
  font-size: 0.9375rem;
  color: var(--text-secondary);
  line-height: 1.6;
}

@media (max-width: 640px) {
  .work-teaser {
    flex-direction: column;
  }
  .work-teaser-media {
    max-width: none;
  }
}
/* ---------- About page: give .about-grid real card chrome ----------
   Scoped with a modifier rather than changed at the base, because the
   homepage's #about section and page-service-areas.php both use the bare
   .about-grid and read correctly as running text there. On a dedicated
   page the same markup was four unstyled text blocks floating in a grid,
   which is what made the page look unfinished. */
.about-grid.is-cards {
  gap: 20px;
}
.about-grid.is-cards > div {
  border: 1px solid var(--border);
  background: var(--card-bg);
  border-radius: 16px;
  padding: 24px;
  transition: border-color 0.3s ease, transform 0.3s ease;
}
.about-grid.is-cards > div:hover {
  border-color: var(--accent-core);
  transform: translateY(-3px);
}
.about-grid.is-cards strong {
  font-family: var(--font-display);
  font-size: 1.0625rem;
  margin-bottom: 8px;
}

@media (max-width: 900px) {
  /* ---------- Why Us: same mobile-only snap carousel as the pricing
     cards above (2026-08-17, per request — "make these cards a slider on
     mobile too"), not the always-on Our Work/Insights pattern -- desktop
     keeps .diff-grid's plain responsive grid, only this breakpoint
     changes. No expand/collapse toggle here (unlike .spark-card) since a
     diff-card's checklist is always fully visible, not collapsed. */
  .diff-grid {
    display: flex;
    grid-template-columns: none;
    gap: 16px;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    scroll-padding-inline: 9vw;
    margin: 0 -20px;
    padding: 10px 9vw 6px;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
  }
  .diff-grid::-webkit-scrollbar {
    display: none;
  }
  .diff-card {
    flex: 0 0 82%;
    scroll-snap-align: center;
    transform: scale(0.94);
    opacity: 0.7;
    transition: border-color 0.3s ease, box-shadow 0.3s ease, transform 0.3s ease, opacity 0.3s ease;
  }
  .diff-card.is-current {
    transform: scale(1);
    opacity: 1;
    border-color: var(--accent-core);
    box-shadow: 0 18px 44px var(--accent-glow);
  }
  .diff-dots {
    display: flex;
  }
  /* Our Work + Insights adopt the same geometry (2026-08-18, per request).
     These two had scroll-snap-align:start with a 12px gutter and a
     clamp(240px,80vw,300px) card, which on a 375px phone left ~31px of the
     next card showing -- a sliver that reads as a clipped card rather than
     as more content. 82% + a 9vw gutter on both sides puts roughly 60px of
     the neighbouring card on screen and centres the active one, which is
     what makes it legible as a carousel. Cards keep their own .is-current
     accent border (already defined at desktop width), so only the layout
     and the dim-when-inactive treatment are new here.

     The dim is milder than .diff-card's 0.7/0.94: these cards lead with a
     photo, and at 0.7 an inactive one reads as still loading. */
  .work-grid, .spark-insights-grid {
    gap: 16px;
    scroll-padding-inline: 9vw;
    margin: 10px -20px 0;
    padding: 10px 9vw 6px;
  }
  .work-card, .spark-insights-card {
    flex: 0 0 82%;
    max-width: none;
    scroll-snap-align: center;
    transform: scale(0.97);
    opacity: 0.82;
  }
  .work-card.is-current, .spark-insights-card.is-current {
    transform: scale(1);
    opacity: 1;
  }
}
@media (max-width: 900px) and (prefers-reduced-motion: reduce) {
  .work-card, .spark-insights-card,
  .work-card.is-current, .spark-insights-card.is-current {
    transform: none;
  }
}
@media (max-width: 900px) {
  .hero {
    padding: 30px 0 50px;
    margin: 0 20px;
  }
  h1 {
    font-size: 2.375rem;
  }
}
@media (max-width: 900px) {
  /* The audit form bar stacks below ~420px of available width -- input and
     button side by side needs room for both, and a cramped button is worse
     than a tall one. .audit-entry itself needs no override now that the
     two-column .audit-row is gone (2026-08-18): it's just a 560px-max block
     under the section header, which the 335px container already bounds. */
  .audit-entry-form {
    flex-wrap: wrap;
  }
  .audit-entry-form input, .audit-entry-form .btn {
    flex: 1 1 100%;
  }
  .section {
    padding: 50px 0;
    margin: 0 20px;
  }
  /* .work-grid's edge-bleed margin/padding (base rule, ~line 348) is
     computed to exactly cancel .section's own fluid margin formula
     (max(40px, calc(50% - 650px))) -- but .section switches to a flat 20px
     here, not that formula, so the base rule's canceling math no longer
     matches and the slider overflowed the viewport by 20px each side
     (2026-08-17, found via horizontal-scroll bug report). Match the actual
     gutter directly rather than trying to keep two independently-computed
     formulas in sync. */
  .work-grid {
    scroll-padding-inline: 20px;
    margin: 10px -20px 0;
    padding: 4px 20px 10px;
  }
  /* Same fix, same reason, applied up front this time instead of found live
     after shipping -- .spark-insights-grid is the same edge-bleed pattern
     as .work-grid immediately above. Renamed from .insights-grid, see that
     rule's own docblock (~line 465) for why. */
  .spark-insights-grid {
    scroll-padding-inline: 20px;
    margin: 10px -20px 0;
    padding: 4px 20px 10px;
  }
  /* v2 (2026-08-19, per request -- "improve this UX", screenshot showed the
     button and reassurance note visibly offset from each other and from the
     heading). Root cause: align-items:flex-start left the row's flex-basis
     items (.cta-banner and .cta-banner-action) each sized to their own
     content instead of the card's full width, so .cta-banner-action shrank
     to its widest child (the button) and .cta-banner-note's own
     text-align:center then centered inside THAT narrow box -- reading as
     randomly indented under a left-aligned heading, not centered under
     anything. align-items:center instead makes the whole card center
     symmetrically, matching the .insight-single-cta pattern the site
     already uses for its other CTA card (~line 2482) rather than inventing
     a second mobile CTA alignment convention. Padding tightened to match
     that same card's mobile figure (26px/32px 20-28px) instead of carrying
     the desktop 50px, which was excess green space on a ~350px-wide card. */
  .cta-banner {
    flex-direction: column;
    align-items: center;
    text-align: center;
    margin: 0 20px 40px;
    padding: 36px 28px;
  }
  /* footer uses padding (not margin, since its border-top needs full-bleed) for the same
     side gutter as .hero/.section/.cta-banner above -- missed in the original mobile pass,
     so it stayed at the desktop 40px while every other section dropped to 20px, making
     footer content visibly narrower/misaligned on mobile. */
  /* Both footer tokens live on this one rule, and the reason is the same for
     each: this block already has a `footer` rule, so a second one is a
     duplicate selector (stylelint catches it, and it caught the first
     attempt at --footer-rule-gap here).

     --f-gap is the gutter shared by the contact grid and the link grid;
     what it means, and why it is tied to the 1.3fr/1fr split, is documented
     at .footer-grid below.

     --footer-rule-gap is the clearance every horizontal rule gets on both
     sides -- 26px at this breakpoint, 32px at desktop; see the token's own
     note at the base `footer` rule. */
  footer {
    padding: 36px 20px;
    font-size: 0.8125rem;
    --f-gap: 32px;
    --footer-rule-gap: 26px;
  }
  /* v4 mobile footer (2026-08-20, per request: "I want it to the left of
     links" -- Contact was stacking full-width above Explore/Quick Links
     instead of sitting beside them like it does at desktop). 3 columns
     now, not 2: Brand/Newsletter still span the full row (grid-column:
     1/-1), but Contact/Explore/Quick Links sit side by side in one row,
     Contact leftmost by DOM order. Tighter gap than the old 2-column
     layout's 24px -- 3 columns in ~350px of content width need it, and
     Contact's own items (min-width:0 + overflow-wrap:anywhere, see
     .footer-contact) already wrap gracefully rather than overflow, so a
     narrower column is a legibility tradeoff, not a broken one. Row-gap
     also cut from the old 2-column layout's 30px to 22px (2026-08-20,
     per request, "reduce empty space") -- 30px read as a bigger break
     than the content on either side of it (one line of tagline, then a
     column heading) actually needed. */
  /* v5 (2026-08-20, per request -- back to icon+text rows for Contact
     after the icon-only pivot, which needs more room than an even 1fr
     split gives it: "WhatsApp" plus a wrapped email address is more
     content than Explore/Quick Links' own short nav labels ever have to
     fit on one line. */
  /* TWO columns with EXPLICIT placement (2026-08-26, per request: "make this
     into a proper, perfectly organized grid").

     Auto-flow was the problem. Three link columns flowing into two tracks
     put Services and Explore on row one and left Quick Links alone on row
     two with the whole right half empty -- and because Services carries five
     real page titles against Explore's four one-word labels, row one was
     also far taller than row two. It read as two unrelated blocks rather
     than one grid.

     Placed by hand instead: Services owns the left track across both rows,
     Explore and Quick Links stack down the right one. Nothing is stranded,
     the two tracks come out close to the same height, and the single divider
     between them runs the whole way down.

     This also RETIRES the :nth-child divider reset that used to live below.
     With the columns placed, the only one at the grid's own left edge is the
     first child, which the desktop rule's :not(:first-child) already skips --
     so there is nothing left to cancel. */
  /* ONE COLUMN AXIS FOR THE WHOLE FOOTER. The contact strip and the link
     columns are two separate grids stacked on top of each other, and until
     now they split their width differently -- contact's second column began
     at x=174 while Explore's began at x=196.5, a 22px stagger between two
     blocks a reader sees as one table.

     THE GUTTER AND THE RATIO ARE ONE DECISION, because the email address
     sets a floor on the first column. It needs 162px at this size, and the
     first column gets (335 - gap) * r1/(r1+r2) at 375px -- so widening the
     gutter narrows it, and past a point the email wraps to two lines in a
     row of one-line cells. 1.3fr / 1fr at a 32px gutter leaves the first
     column 171px, which clears 162 with real slack, and the second 132,
     which is comfortably more than the longest label in it ("Service Areas",
     ~85px). An even 1fr/1fr split cannot hold both: it caps the gutter at
     11px before the email breaks.

     The 32px gutter is what the request "not right against the left divider"
     asked for (2026-08-26). The divider sits in the middle of it, so the
     columns clear it by 16px on BOTH sides rather than the 9px they had --
     symmetric, and the same number on each, which is the "consistent" half.

     --f-gap and the 1.3/2.3 in the divider below are THE SAME NUMBERS as
     this template. Change the ratio here and the divider stops sitting in
     the gap; it is a straight line either way, so nothing will look broken,
     it will just be wrong. */
  /* The row-gap READS --footer-rule-gap rather than restating 26px: the
     three rules inside this grid take their upper clearance from that gap
     and their lower clearance from the token, so were the two numbers ever
     allowed to differ, all three would go lopsided at once with nothing in
     the file pointing at why. The token itself is set on the `footer` rule
     at the top of this block. */
  .footer-grid {
    grid-template-columns: 1.3fr 1fr;
    gap: var(--footer-rule-gap) var(--f-gap);
  }
  /* TWO COLUMNS AND A STRIP (2026-08-26, per request: "any way we can
     improve this so the columns are similar/equal length so everything looks
     symmetrical?").

     It was Services spanning both rows of the left track with Explore over
     Quick Links in the right. Measured at 700px: the right track ran the
     full 296px while Services' own content stopped at 183, so the left
     column sat beside a 113px hole. Both TRACKS ended level -- the grid
     stretched them -- which is why this reads as a hole rather than as a
     ragged edge, and why align-items could never fix it.

     NO REGROUPING OF THE LINKS CAN FIX IT EITHER, which is what forces a
     layout change. The columns hold 5, 4 and 3 links, so the three ways to
     split them across two tracks are 5|7, 8|4 and 9|3 -- and the one that
     was already in place is the closest of the three. The imbalance is in
     the 5/4/3, not in the arrangement.

     So Quick Links leaves the column system: Services and Explore share row
     one (183 vs 151 -- a 32px difference reads as level), and Quick Links
     becomes a full-width strip beneath them. Its three labels are the
     shortest in the footer and total ~185px, so they set on one line at
     every width down to 320. There is precedent directly above it: the
     contact row is already a horizontal strip in this same footer, so a
     strip is not a new idea here, and Quick Links is the right column to
     spend on it -- it is the "everything else" group by its own docblock's
     account, not one of the two the reader is scanning. */
  /* FIVE children (2026-08-26 v7): brand, Services, Explore, Quick Links,
     Contact. THESE RULES DO NOT FAIL WHEN STALE -- they silently style the
     wrong column -- and they have now been renumbered three times in one
     day, so trust the count in this comment and re-check every index if
     anything is added to or removed from this grid.

     Brand takes row one on its own: it holds an 88px mark and a tagline, and
     there is nothing to pair it with that would not be squeezed.
     Services and Explore pair off in row two -- the only two whose labels
     fit a 171px track. Quick Links is a full-width strip in row three; its
     three labels are the shortest here (~185px total) and hold one line down
     to 320px. Contact takes row four full-width because the email address
     needs ~162px on one line, which no half-track at this width can give it.

     Only Explore carries a divider, since it is the only column with another
     beside it here. */
  /* ROW DIVIDERS (2026-08-26, per request: "maybe add some row dividers here
     too where appropriate"). Stacked, these four blocks ran together with
     nothing but a gap between them, while the one pair that IS side by side
     had a rule between it -- so the footer looked divided across but not
     down.

     EACH LINE IS DRAWN BY A FULL-WIDTH BLOCK, never by the two half-width
     ones. A border-top on Services AND Explore together would draw the same
     line twice with the 32px column gap punched out of the middle of it -- a
     divider with a hole, which this footer has already shipped once (see the
     retired .footer-grid::before note below). So the row-1/row-2 boundary is
     drawn as brand's border-BOTTOM, and the two below it as Quick Links' and
     Contact's border-TOP.

     The 26px padding matches the grid's own 26px row-gap, so each line sits
     with equal space above and below rather than hugging whichever block
     owns it. */
  .footer-grid > .footer-col:nth-child(1) {
    grid-column: 1/-1;
    grid-row: 1;
    border-left: 0;
    margin-left: 0;
    padding-left: 0;
    border-bottom: 1px solid var(--border);
    padding-bottom: var(--footer-rule-gap);
  }
  .footer-grid > .footer-col:nth-child(2) {
    grid-column: 1;
    grid-row: 2;
    border-left: 0;
    margin-left: 0;
    padding-left: 0;
  }
  /* 16px is half of --f-gap, so the line sits centred in the gutter with
     equal clearance on both sides -- the desktop rule uses half of its own
     gutter for the same reason. */
  .footer-grid > .footer-col:nth-child(3) {
    grid-column: 2;
    grid-row: 2;
    border-left: 1px solid var(--border);
    margin-left: -16px;
    padding-left: 16px;
  }
  .footer-grid > .footer-col:nth-child(4) {
    grid-column: 1/-1;
    grid-row: 3;
    border-left: 0;
    margin-left: 0;
    padding-left: 0;
    border-top: 1px solid var(--border);
    padding-top: var(--footer-rule-gap);
  }
  .footer-grid > .footer-col:nth-child(5) {
    grid-column: 1/-1;
    grid-row: 4;
    border-left: 0;
    margin-left: 0;
    padding-left: 0;
    border-top: 1px solid var(--border);
    padding-top: var(--footer-rule-gap);
  }
  /* Row, not column: Quick Links is a strip under two columns, not a third
     column. The 20px column-gap is deliberately NOT --f-gap -- that variable
     is the gutter a divider is centred in, and there is no divider here;
     these are inline links that only need separating from each other. */
  .footer-grid > .footer-col:nth-child(4) .footer-nav-list {
    flex-direction: row;
    flex-wrap: wrap;
    gap: 0 20px;
  }
  /* Side by side again at this width: full-width, the lockup and the tagline
     fit on one row, and stacking them would spend ~70px of height for
     nothing. The desktop rule stacks them because there it is one narrow
     column among five. */
  .footer-brand-row {
    flex-direction: row;
    align-items: center;
    gap: 18px;
  }
  /* The card's own stacked padding is set further down this block, where
     .footer-signup already collapses to one column -- not repeated here. */
  /* .footer-col-brand is gone for good (2026-08-26 v4): the brand lockup and
     the contact list are .footer-top-brand now, the left half of the zone
     above this grid, so there is no track for it to span. */
  .footer-logo-img {
    height: 72px;
  }
  .footer-blurb {
    max-width: none;
    font-size: 0.8125rem;
  }
  /* Decreased further (2026-08-20, per request, alongside the 3-column
     squeeze above) -- .footer-nav-list a in particular was rendering at
     18px here, not the 13px its own 0.8125rem base rule implies: two
     later sitewide "mobile readability" passes (~line 2157, ~line 2190)
     each bump it further and both win the cascade over this earlier
     block's own plain-class rules by source order. Ancestor-qualified
     selectors here (.footer-grid ..., not the bare class) add specificity
     instead of fighting source order, so these stay correct regardless of
     where either pass ends up in the file. Nav links and contact items
     unified at the same size -- there was no reason for Explore/Quick
     Links to run larger than Contact right next to them. */
  .footer-grid .footer-nav-list a {
    font-size: 0.75rem;
  }
  /* .footer-contact-row, not .footer-grid (2026-08-26). Contact stopped
     being a grid column earlier the same day, which silently took this half
     of the rule out of the cascade -- the contact items had been rendering
     at 14.625px next to nav links at 13.5px ever since, which is exactly the
     mismatch the sentence above says was unified. A descendant selector goes
     quiet, not wrong, when its ancestor moves. */
  .footer-contact-row .footer-contact a,
  .footer-contact-row .footer-contact-loc {
    font-size: 0.75rem;
  }
  footer .footer-copy {
    font-size: 0.75rem;
  }
  /* The .footer-grid::before divider that used to sit here is GONE
     (2026-08-26), and so is the `position: relative` on .footer-grid that
     existed only to anchor it.

     It was drawn on the grid, with its x computed from the same 1.3/2.3 and
     --f-gap as the template, because Explore and Quick Links shared the
     right-hand track: a border-left on each drew the same line twice with
     the 26px row-gap punched out of the middle -- a divider with a hole in
     it. Quick Links is a full-width strip in row 2 now, so the right track
     holds exactly one column, and a border on that column is both correct
     and self-sizing. It spans row one and stops, where a grid-height rule
     would have run on down past the strip below it and needed its own
     height calculation to avoid that.

     This also retires a coupling worth being rid of: the divider no longer
     has to be re-derived whenever the column ratio or the gutter changes. */
  /* The :nth-child divider reset that used to sit here is gone (2026-08-26).
     It existed to strip the border-left off whichever column happened to
     land at the grid's own left edge, which under auto-flow alternated and
     had to be re-derived (even, then odd) every time anything above it
     moved. With the columns placed explicitly above, the left track holds
     exactly one child and it is :first-child, which the desktop divider rule
     already excludes. Nothing to cancel. */
  /* The .footer-col-contact rule that used to sit here is gone (2026-08-26):
     Contact is no longer a grid column at all, it is .footer-contact-row above
     this grid. The nth-child(even) rule above covers what it did. */
  /* A rule above the newsletter block so it reads as its own thing rather
     than as a third link column that ran out of links. Also cancels the
     desktop side-divider (border-left/margin-left/padding-left above) --
     full-width here, a left border would just be a stray vertical line. */
  /* .footer-grid > .footer-col.footer-col-news, not just .footer-col-news --
     the desktop divider rule above (.footer-grid > .footer-col:not(:first-
     child)) carries 3 class-level selectors' worth of specificity, which
     beat a bare .footer-col-news here and silently kept its border-left on
     even at this breakpoint. Matched, not brute-forced with !important.
     margin-left: 0 for the same reason as Explore's rule above -- missing
     it here left the newsletter block flush against the viewport edge too. */
  /* .footer-col-news is gone (2026-08-26) -- the newsletter is no longer a
     grid column at all, it is .footer-signup, a full-width band below this
     grid with its own card treatment at every width. The rule that used to
     sit here gave it a top rule and cancelled the desktop side-divider;
     both are now moot. */
  .footer-signup {
    grid-template-columns: 1fr;
    gap: 20px;
    padding: 24px;
    margin-bottom: 30px;
  }
  /* The `.footer-local { margin-top: 32px }` that used to sit here is gone
     (2026-08-26): the local line is the first line INSIDE .footer-bottom
     now, not a band above it, and a top margin here reopened exactly the
     gap that merging the two zones closed. */
  .about-grid {
    grid-template-columns: 1fr;
  }
  .form-row {
    grid-template-columns: 1fr;
  }
  header {
    padding: 14px 16px;
  }
  /* Kept visible at every breakpoint (2026-08-19, per request) -- it was
     hidden here before, which directly contradicted header.php's own
     docblock ("the hard [CTA] one -- 'Book a Call', in the header bar --
     is always visible"). Shrinking its own padding/font-size (below)
     wasn't enough on a real phone, though -- confirmed live on
     dev.kmcspark.com at 390px: hamburger + logo + theme toggle + button
     didn't fit on one line even with the button shrunk, so both the logo
     and the button wrapped their own text onto multiple lines. Fixed by
     freeing width instead of shrinking further: the theme toggle drops
     its text label at this breakpoint (icon-only, same treatment the
     hamburger button's own "Menu" label used to get by default before it
     was removed entirely, below), and nowrap on both the logo and the
     button now means something, because there's finally room for it to
     hold. */
  .nav-right .btn-primary {
    padding: 9px 16px;
    font-size: 0.8125rem;
    white-space: nowrap;
  }
  /* font-size removed 2026-08-25: the round-2 block below sets 1.0625rem
     in an identical @media, so the 1rem here never applied. NOTE that
     means the nav-fitting fix this comment describes only ever got the
     nowrap and the freed width, never the smaller logo. */
  .logo {
    white-space: nowrap;
    font-size: 1.0625rem;
  }
  /* The two rules that used to sit here -- `.theme-toggle-label { display:
     none }` and a tighter `.theme-toggle` padding -- were both aimed at the
     HEADER toggle, which no longer exists (2026-08-26). Left in, they applied
     to the tray button instead, where hiding the word is exactly wrong: the
     tray is where the label earns its place. */
  .nav-right {
    gap: 10px;
  }
  /* .nav-left holds the hamburger AND the wordmark again as of 2026-08-26,
     so its gap is a live lever once more -- it is set to 12px on the base
     rule and deliberately not overridden here. The bar has one item fewer
     than it did when this needed tightening (the theme control moved into
     the tray), so there is width to spare at these sizes. */
  .contact-book {
    padding: 22px;
  }
  .contact-book h2 {
    font-size: 1.3125rem;
  }
  .audit-card {
    padding: 22px;
  }
  /* Body copy sat at 11-14px on mobile with no adjustment from desktop --
     readable on a laptop, too small once actually held in a hand.

     MOST OF THIS BLOCK IS GONE as of 2026-08-25. A second
     @media (max-width: 900px) block ("round 2") further down set
     font-size for twelve of the same selectors, and being later with
     equal specificity it won every one of them -- so round 1's values
     had not applied since round 2 was written. Round 2 was authored
     believing it "stacked on" this block; it replaced it.

     Deleting the dead declarations changed no rendered value (verified
     by measuring computed font-size, line-height and white-space at
     400px and 1280px before and after). What survives here is what
     round 2 does NOT redeclare. */
  .accordion-body p {
    line-height: 1.65;
    font-size: 1.0625rem;
  } /* font-size folded in from the round-2 duplicate below (2026-08-25) */
  /* ---------- Mobile readability pass (round 2): first pass wasn't enough, and missed
  several UI-text classes entirely (buttons, eyebrow pills, card headings, nav labels).
  All font-size values sitewide are in rem now (16px root) -- these mobile overrides
  bump the rem value itself, so they stack on top of the rem conversion, not replace it. ---------- */
  .hero p.sub {
    font-size: 1.0625rem;
  }
  .btn {
    font-size: 1rem;
  }
  .eyebrow {
    font-size: 0.8125rem;
  }
  /* .why-us-lede rides along -- it is .section-lead's twin as of 2026-08-26
     (see its own rule), and left out of this it rendered 2.25px smaller than
     every other section lead on a phone. */
  .section-lead,
  .why-us-lede {
    font-size: 1.0625rem;
  }
  .accordion-title strong {
    font-size: 1.125rem;
  }
  .timeline-step strong {
    font-size: 1.0625rem;
  }
  .timeline-step p {
    font-size: 1rem;
  }
  .pricing-footnote {
    font-size: 0.9375rem;
  }
  .about-grid strong {
    font-size: 1rem;
  }
  .about-grid p {
    font-size: 1rem;
  }
  .diff-card h3 {
    font-size: 1.25rem;
  }
  .diff-card > p {
    font-size: 0.9375rem;
  }
  .diff-card li {
    font-size: 0.9375rem;
  }
  .cta-banner p {
    font-size: 1rem;
  }
  .contact-form label {
    font-size: 0.875rem;
  }
  .contact-form input, .contact-form select {
    font-size: 1.0625rem;
  }
  .footer-nav-list a {
    font-size: 1rem;
  }
  .footer-copy {
    font-size: 0.8125rem;
  }
  /* Same 20px mobile gutter as .section/.cta-banner (see the generic 900px
  block above) -- placed here, AFTER the base .industry-hero/.back-link-row
  rules above, on purpose: an identical override once lived inside that
  earlier block instead and silently never applied. Same specificity (one
  class selector each), so CSS cascade ties resolve by source order --
  sitting before the unconditional base rule in the file meant the base
  rule's 40px always won regardless of viewport width. */
  .breadcrumbs {
    margin: 0 20px;
  }
  .industry-hero {
    margin: 18px 20px 20px;
  }
  /* ---------- Mobile / tablet overrides for everything above ---------- */
  /* Single column. The brief panel STAYS VISIBLE here -- it is text, so
     it costs nothing to fetch, and it is the only thing standing between
     a phone visitor and a hero that is otherwise a headline, a paragraph
     and two buttons. (Its predecessor, a client screenshot, was hidden at
     this width precisely because an image is not free; see the .has-brief
     docblock above.) */
  .hero.has-brief {
    grid-template-columns: 1fr;
    gap: 0;
  }
  .hero .hero-content h1,
  .hero .hero-content p.sub {
    max-width: none;
  }
  .hero-brief {
    margin-top: 4px;
    padding: 22px;
  }
  .hero-brief-list {
    gap: 14px;
  }
  .hero-brief-list strong {
    font-size: 1.0625rem;
  }
  .hero-brief-list span {
    font-size: 0.9375rem;
  }
  /* ---------- Section delineation (2026-08-24, per request: the mobile
     page "needs to be structured differently. maybe some indicators/
     distinctions between content areas/sections").

     On desktop each section is separated by 140px of combined padding and
     the eye reads that as a break. At 900px that collapses to 100px, on a
     viewport a third as wide -- so eight sections run together as one
     undifferentiated column of text and the reader loses track of where
     one subject ends and the next begins. A hairline plus the section's
     own eyebrow label restores the boundary without adding height.

     Deliberately NOT applied at desktop widths: the request was
     specifically about mobile, the desktop rhythm already works, and
     sitewide chrome is a much larger change than a mobile-only one.

     The border sits on .section, which carries the same 20px side margin
     at this width, so it stops at the text column rather than bleeding to
     the viewport edge -- it reads as a rule under the content, not as a
     full-bleed band. .cta-banner is exempt on both sides: it is a solid
     lime block, already the strongest divider on the page, and a hairline
     immediately after it would be noise. ---------- */
  #main > .section {
    border-top: 1px solid var(--border);
  }
  #main > .cta-banner + .section {
    border-top: none;
  }
  /* The section label is the other half of the boundary -- it says WHAT
     the new area is, where the rule only says THAT it is new. At 12px it
     was doing that job too quietly to register on a phone. */
  .eyebrow-kicker {
    font-size: 0.8125rem;
    letter-spacing: var(--label-tracking);
  }
  .hero-sub {
    padding: 28px 0 4px;
  }
  .hero-sub h1 {
    max-width: none;
  }
  .why-us-card h3 {
    font-size: 1.25rem;
  }
  .why-us-card > p {
    font-size: 0.9375rem;
  }
  .why-us-card li {
    font-size: 0.9375rem;
  }
  .about-grid.is-cards strong {
    font-size: 1.0625rem;
  }
}
@media (max-width: 480px) {
  /* ---------- Phone-width hero compaction (2026-08-21) ----------
  The 900px breakpoint above still leaves the hero (eyebrow + 5-line H1 +
  lead paragraph) covering most of a phone's first screen with no visual
  break before the CTA -- confirmed against a mobile screenshot review.
  hero_lead itself was shortened in functions.php
  (kmc_spark_shorten_hero_lead_mobile_v1), but the H1 stays at full length
  deliberately (it carries SEO keywords -- see that migration's own
  docblock), so the remaining lever is tightening size/spacing at true
  phone widths rather than the 900px tablet cutoff shared with .section
  etc. Scoped to .hero only -- .industry-hero's shorter one-line H1 doesn't
  have this problem. */
  /* padding-bottom is 24px, not the 32px this rule carried until
     2026-08-25: a second `.hero { padding-bottom: 24px }` used to sit in a
     duplicate 480px block further down and quietly won. Same computed
     result, one rule. */
  .hero {
    padding: 16px 0 24px;
  }
  .eyebrow {
    margin-bottom: 10px;
  }
  /* HIERARCHY (2026-08-25, per request: "the header should be bigger and sub
     head smaller").

     Measured before changing anything, because the two rules had drifted
     independently and the gap was much worse than it looked: at 375px the H1
     computed to 29.25px against an 18px lead -- a ratio of 1.61, where the
     SAME two elements on desktop sit at 3.28 (58.5 / 18). The lead was
     nearly the size of the headline, so the fold had no clear first thing to
     read. Note the root font-size here is 18px, not the usual 16, so every
     rem value on this site is 1.125x what it looks like.

     Both halves move, since closing the gap from one side alone would mean
     either a headline that wraps to five lines or a lead below the site's
     own body size. 2.125rem (38.25px) against 0.875rem (15.75px) puts the
     ratio at 2.43 and keeps the lead at exactly the body size used
     everywhere else on the page.

     HISTORICAL NOTE, kept because it is the reason this file now has one
     block per breakpoint: the lead's size originally could NOT be set here.
     There were five competing `.hero p.sub` font-size rules across three
     breakpoints, and the winner lived in a SECOND `@media (max-width:
     480px)` block further down -- so the first attempt at this change was
     silently overridden and looked like it had done nothing. That duplicate
     block is gone (2026-08-25) and the lead's size is set right below,
     where you would expect to find it. */
  .hero h1 {
    font-size: 2.125rem;
    line-height: 1.12;
    margin-bottom: 12px;
  }
  /* The lead. 0.9375rem (16.875px) as of 2026-08-25, per request ("the
     header should be bigger and sub head smaller"), down from 1rem/18px.
     The headline moved in the same pass, 1.625rem -> 2.125rem, taking the
     H1:lead ratio at 375px from 1.61 to 2.27; desktop has always sat at
     3.28, so the fold on a phone had no clear first thing to read.

     NOTE the root is 112.5% (18px), not 16px, so every rem here is 1.125x
     what it reads as.

     16.875px is under the 18px BASE, but still larger than the 0.875rem
     (15.75px) this file uses for real body copy in .included-card li and
     the Why Us contrast rows -- so it is not the
     smallest text on the page, and it is a lead paragraph, not body copy. */
  .hero p.sub {
    font-size: 0.9375rem;
    line-height: 1.5;
    margin-bottom: 16px;
  }
  /* Fold tightening. The `.hero` padding-bottom and `.hero p.sub` sizing
     that used to live here were folded into their counterparts at the top
     of this block (2026-08-25) when the duplicate 480px block was merged
     away -- same computed values, one rule each. */
  .hero-ctas {
    gap: 10px;
    margin-bottom: 26px;
  }
  .hero-ctas .btn {
    flex: 1 1 100%;
    text-align: center;
  }
  /* The sticky header's "Book a Call" and the hero's "Book My Free
     Consultation" are the SAME destination (#contact) under two names,
     and both were filled lime, so the fold showed what looked like two
     competing offers. The hero's is the one with the room to say what it
     is and to carry the "30-minute call, no obligation" reassurance under
     it, so the header's steps back to a ghost while they share a screen.

     Scoped to body.home, which is the only place a hero CTA exists. On
     /why-us/, /about/, /faq/ and /free-audit/ the header button is the
     ONLY call to action above the fold and it stays filled there.

     Not hidden outright: the header is sticky, so this button is the
     persistent CTA for the whole rest of the page. It has to stay visible
     and tappable -- it only has to stop shouting over the hero.

     IT IS JS-driven now, as of the .header-cta-solid rule directly below
     (pre-promotion audit CONV-04). Ghosted for the WHOLE page was the gap
     that finding was really pointing at: a persistent-but-illegible CTA is
     barely more useful than no CTA once you're screens past the hero. */
  body.home header .nav-right .btn-primary {
    background: none;
    color: var(--text);
    border-color: var(--border);
  }
  body.home header .nav-right .btn-primary:hover {
    box-shadow: none;
    border-color: var(--accent-core);
  }
  /* Restores the default filled look the instant .hero scrolls out from
     under the sticky header (main.js's IntersectionObserver toggles this
     class on body). Ghosted only while the hero's own CTA is sharing the
     screen; solid for every scroll depth after that, which is most of an
     11.9-screen page. Higher specificity than the ghost rule above via the
     extra class, so no !important needed and load order doesn't matter. */
  body.home.header-cta-solid header .nav-right .btn-primary {
    background: var(--accent-core);
    color: var(--accent-on);
    border-color: transparent;
  }
  body.home.header-cta-solid header .nav-right .btn-primary:hover {
    box-shadow: 0 0 24px var(--accent-glow);
    border-color: transparent;
  }
  /* ---------- Fold density (2026-08-24, per request: "on mobile the
     above the fold is way too busy and confusing").

     Counted on a 390x844 screen, the fold held eleven distinct pieces of
     chrome: a lime header button, a lime pill, a headline with a lime
     phrase in it, a four-line paragraph, a lime button, an underlined
     link, a reassurance note, and a card whose title and four bullets are
     also lime -- plus the chat bubble. Three of those are calls to action
     competing with each other, and the brand lime appears six times, so
     nothing is emphasised because everything is. The two changes below
     remove weight rather than content.

     Not touched, deliberately: hero_lead still opens "Piscataway, NJ --"
     directly under an eyebrow that already reads "... PISCATAWAY, NJ", so
     the location is stated twice in consecutive lines. That is a copy
     edit to a seeded ACF value carrying a deliberate local-then-remote
     pairing (see CLAUDE.md, 2026-08-17), not a CSS fix, so it is flagged
     rather than done here. ---------- */
  /* 1. The eyebrow is a LABEL, not an action -- but as a filled lime pill
        it is the same shape, colour and weight as the two real buttons on
        the screen, so it reads as a third one. Text-only keeps the
        information and drops the false affordance.
        --accent-core-text, not the raw brand lime, which measures about
        1.7:1 as text on the light background. */
  .hero .eyebrow {
    background: none;
    color: var(--accent-core-text);
    padding: 0;
    margin-bottom: 12px;
    letter-spacing: 1.4px;
  }
  /* 2. The brief drops to its labels. Each label is already the whole
        differentiator -- "Real design, not templates" states the claim
        and the thing it beats in four words -- and the sentence under it
        was elaboration, which is what #why-us is for: it carries the full
        ~25-word tagline for every one of these four, so nothing is lost
        from the page, only from the fold. Four scannable lines instead of
        sixteen takes the panel from 536px to roughly 200px, which is what
        lets the whole hero resolve inside one screen instead of being cut
        mid-card. */
  .hero-brief {
    padding: 20px 22px;
  }
  .hero-brief-title {
    margin-bottom: 14px;
  }
  .hero-brief-list {
    gap: 12px;
  }
  .hero-brief-list span {
    display: none;
  }
  .hero-brief-list strong {
    font-size: 1rem;
  }
  .subpage-back {
    margin-bottom: 12px;
  }
}
@media (prefers-reduced-motion: reduce) {
  .why-us-card:hover,
  .about-grid.is-cards > div:hover {
    transform: none;
  }
}
/* ---------- Audit request state on the contact form ----------
   (2026-08-24, per request: "with a similar lock as we have for the
   calendar so that all contact info flows into one unified form".)

   The banner that appears at the top of #contactForm when a visitor arrives
   there through the mid-page #audit hook, and the matching confirmation line
   in the success panel. See front-page.php's #audit v6 docblock for why the
   audit routes here at all.

   Deliberately quieter than .form-success: this sits ABOVE a form the
   visitor still has to fill in, so it has to read as context for the work
   ahead, not as a reward already collected. A tinted left edge rather than a
   full accent border does that -- .contact-book-lock next to it is already
   the loud "you have not done the thing yet" element in this section, and
   two of those competing would just look like an error state. */
.form-audit-request {
  border-left: 3px solid var(--accent-core);
  /* Literal rgba of --accent-core (#8CD211) rather than color-mix(): this
     would be the only color-mix() in the file, and where it is not
     supported the declaration is simply dropped, leaving the banner
     untinted. An alpha rgba works everywhere and reads identically on both
     themes, since it composites over whatever --card-bg is. */
  background: rgba(140, 210, 17, 0.07);
  border-radius: 0 10px 10px 0;
  padding: 14px 16px;
  margin-bottom: 4px;
}

/* The same trap .form-success and .contact-form both hit: a display rule
   here would beat the browser's default `[hidden] { display: none }` and
   render this banner on every page load, for every visitor, promising an
   audit nobody asked for. It does not set `display` today -- this guard is
   so that it stays safe if someone later adds one. */
.form-audit-request[hidden] {
  display: none;
}

.form-audit-request-title {
  font-family: var(--font-display);
  font-size: 0.9375rem;
  /* 600, not 700: this is a card-rank title (see .why-us-teaser-card h3's
     note). 700 is reserved for h1/h2 section headings. The <strong> inside
     it -- the host being audited -- still steps up, one notch not two. */
  font-weight: 600;
  margin: 0 0 4px;
  display: flex;
  align-items: baseline;
  gap: 7px;
}

/* strong, not a colour change: --accent-core measures about 1.7:1 on the
   light theme's --card-bg and is never safe as text (see this file's own
   note on --accent-core-text). The host is emphasised by weight and by the
   accent already carried on the border and the icon. */
.form-audit-request-title strong {
  font-weight: 700;
}

/* Now wraps an inline SVG rather than the "⚡" emoji (2026-08-25) --
   see front-page.php. display:flex so the 15px glyph centres on the text's
   cap height instead of sitting on the baseline the way an emoji glyph did;
   an inline SVG has no descender to sit on, so without this it rides low. */
.form-audit-request-icon {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  color: var(--accent-core-text);
}

/* Same baseline problem inline: the bolt is mid-sentence here rather than a
   flex sibling, so it needs an explicit nudge onto the text's optical centre. */
.form-success-audit-icon {
  vertical-align: -2px;
  color: var(--accent-core-text);
}

.form-audit-request-note {
  font-size: 0.8125rem;
  line-height: 1.55;
  color: var(--text-secondary);
  margin: 0;
  /* Measure cap. The form is two columns wide on desktop (~1800px at
     1440), and without this the sentence rendered as one very long line
     -- the same over-long-measure problem the hero pass fixed. */
  max-width: 64ch;
}

/* Element+class, matching .form-success-book directly above it in this
   file, so it outweighs `.form-success p { margin: 0 }` on specificity
   rather than needing !important. */
.form-success p.form-success-audit {
  max-width: none;
  width: 100%;
  color: var(--text);
  font-size: 0.875rem;
}
.form-success p.form-success-audit[hidden] {
  display: none;
}
.form-success p.form-success-audit strong {
  font-weight: 800;
}

/* ==========================================================================
   DISCOVERY QUIZ (/start/)
   See kmcspark/wp-content/themes/kmcspark/page-start.php and
   inc/discovery-quiz.php.

   THE PER-CLIENT ACCENT ARRIVES AS CUSTOM PROPERTIES ONLY. page-start.php
   prints --quiz-accent / --quiz-accent-text / --quiz-accent-on /
   --quiz-accent-rgb on .quiz-page and nothing else; every rule that consumes
   them is here. That is the sanctioned handoff pattern in CLAUDE.md's asset
   architecture rule, and the same one single-industries.php uses for
   --cat-color.

   --quiz-ink IS THE ONE TO REACH FOR WHEN THE ACCENT IS TEXT, and the split
   is not cosmetic. Contractors orange (#E8752E) measures 2.99:1 against the
   light theme's --bg, which fails even the 3:1 floor for large text, let
   alone the 4.5:1 normal text needs. So the bright value stays for fills,
   borders and glows -- where it has no contrast requirement of its own -- and
   --quiz-ink resolves to the darkened sibling in light mode. Exactly the same
   problem, and the same fix, as this theme's --accent-core-text.
   ========================================================================== */
.quiz-page {
  --quiz-ink: var(--quiz-accent);
  padding: 40px 0 80px;
}

/* Two selectors, (0,2,0) against .quiz-page's (0,1,0), so this wins on
   specificity rather than on being written later in the file. This codebase
   has been bitten twice by a single-class override that only worked because
   of source order -- see the .insight-card-title and .diff-card h3 notes. */
[data-theme=light] .quiz-page {
  --quiz-ink: var(--quiz-accent-text);
}

.quiz-shell {
  max-width: 720px;
  margin: 0 auto;
  padding: 0 20px;
}

.quiz-card {
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 18px;
  padding: 36px 34px 30px;
  /* The card is a focus target -- App.jsx moves focus here on every step
     change so a screen reader is told the question changed. It must never
     show a browser default outline for that programmatic focus, but it must
     still show one for a real keyboard visit. :focus-visible is exactly that
     distinction, so it is left to do its job and only :focus is silenced. */
}
.quiz-card:focus {
  outline: none;
}
.quiz-card:focus-visible {
  outline: 3px solid var(--quiz-accent);
  outline-offset: 4px;
}
@media (max-width: 480px) {
  .quiz-card {
    padding: 26px 20px 22px;
    border-radius: 14px;
  }
}

/* --------------------------------------------------------------------------
   Progress
   -------------------------------------------------------------------------- */
.quiz-progress {
  max-width: 720px;
  margin: 0 auto 18px;
  padding: 0 20px;
}

.quiz-progress-track {
  height: 6px;
  border-radius: 999px;
  background: var(--field-bg);
  border: 1px solid var(--border);
  overflow: hidden;
}

.quiz-progress-fill {
  height: 100%;
  background: linear-gradient(90deg, var(--quiz-accent), var(--accent-highlight));
  border-radius: 999px;
  transition: width 260ms ease;
}
@media (prefers-reduced-motion: reduce) {
  .quiz-progress-fill {
    transition: none;
  }
}

.quiz-progress-label {
  margin: 8px 0 0;
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-secondary);
}

/* --------------------------------------------------------------------------
   Headings and intro
   -------------------------------------------------------------------------- */
.quiz-eyebrow {
  font-size: var(--label-size);
  font-weight: var(--label-weight);
  letter-spacing: var(--label-tracking);
  text-transform: uppercase;
  color: var(--quiz-ink);
  margin: 0 0 10px;
}

.quiz-intro-title,
.quiz-question-title,
.quiz-reveal-title {
  font-family: var(--font-display);
  font-weight: 700;
  line-height: 1.2;
  color: var(--text);
  margin: 0 0 12px;
  font-size: clamp(1.4rem, 4.4vw, 1.85rem);
}

.quiz-intro-body,
.quiz-question-prompt,
.quiz-reveal-lead {
  color: var(--text);
  margin: 0 0 16px;
  line-height: 1.6;
}

.quiz-question-help,
.quiz-field-help {
  color: var(--text-secondary);
  font-size: 0.85rem;
  margin: 0 0 16px;
  line-height: 1.55;
}

.quiz-intro-meta {
  margin: 14px 0 0;
  font-size: 0.8rem;
  color: var(--text-secondary);
}

/* A question fades in on arrival. App.jsx gives QuestionScreen a key of the
   question index so React remounts it, which is what re-triggers this -- a
   plain re-render of the same node would not. */
.quiz-question,
.quiz-contact,
.quiz-reveal {
  animation: quizStepIn 260ms ease both;
}
@media (prefers-reduced-motion: reduce) {
  .quiz-question,
  .quiz-contact,
  .quiz-reveal {
    animation: none;
  }
}

@keyframes quizStepIn {
  from {
    opacity: 0;
    transform: translateY(6px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}
/* --------------------------------------------------------------------------
   Options
   -------------------------------------------------------------------------- */
.quiz-fieldset {
  border: 0;
  margin: 0 0 18px;
  padding: 0;
}

.quiz-legend {
  padding: 0;
  margin: 0 0 12px;
  font-weight: 700;
  color: var(--text);
  line-height: 1.5;
}

.quiz-options {
  display: grid;
  gap: 10px;
  margin: 0 0 18px;
}

.quiz-options-cards {
  grid-template-columns: repeat(2, 1fr);
}
@media (max-width: 480px) {
  .quiz-options-cards {
    grid-template-columns: 1fr;
  }
}

.quiz-option {
  position: relative;
  display: block;
  cursor: pointer;
}

/* Visually hidden, NOT display:none and NOT hidden -- the input has to stay
   focusable and announceable, because it is the real control. */
.quiz-option-input {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: 0;
  padding: 0;
  opacity: 0;
  pointer-events: none;
}

.quiz-option-face {
  display: flex;
  align-items: center;
  gap: 12px;
  /* 44px floor on the tap target, per the brief's accessibility line. The
     padding alone gets close; min-height guarantees it for a one-word
     option like "Photos". */
  min-height: 44px;
  padding: 12px 16px;
  border: 1px solid var(--field-border);
  border-radius: 12px;
  background: var(--field-bg);
  color: var(--text);
  line-height: 1.45;
  transition: border-color 140ms ease, background 140ms ease, transform 140ms ease;
}
@media (prefers-reduced-motion: reduce) {
  .quiz-option-face {
    transition: none;
  }
}

.quiz-option-input:focus-visible + .quiz-option-face {
  outline: 3px solid var(--quiz-accent);
  outline-offset: 2px;
}

.quiz-option:hover .quiz-option-face {
  border-color: var(--quiz-accent);
}

.quiz-option.is-selected .quiz-option-face {
  border-color: var(--quiz-accent);
  background: rgba(var(--quiz-accent-rgb), 0.12);
}

/* An option refused because the "pick up to 3" limit is reached. It stays
   focusable and stays in the tab order on purpose (see OptionList.jsx) -- this
   only signals the refusal, it does not enforce it. */
.quiz-option.is-blocked .quiz-option-face {
  opacity: 0.45;
  cursor: not-allowed;
}

.quiz-option-tick {
  flex: none;
  width: 20px;
  height: 20px;
  border-radius: 6px;
  border: 2px solid var(--field-border);
  background: var(--card-bg);
  position: relative;
}

.quiz-option.is-selected .quiz-option-tick {
  border-color: var(--quiz-accent);
  background: var(--quiz-accent);
}
.quiz-option.is-selected .quiz-option-tick::after {
  content: "";
  position: absolute;
  left: 5px;
  top: 1px;
  width: 5px;
  height: 10px;
  border: solid var(--quiz-accent-on);
  border-width: 0 2px 2px 0;
  transform: rotate(45deg);
}

.quiz-option-emoji {
  flex: none;
  font-size: 1.4rem;
  line-height: 1;
}

.quiz-option-text {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.quiz-option-sub {
  font-size: 0.78rem;
  color: var(--text-secondary);
}

.quiz-options-cards .quiz-option-face {
  flex-direction: column;
  align-items: flex-start;
  min-height: 92px;
  padding: 16px;
}

/* --------------------------------------------------------------------------
   Text inputs
   -------------------------------------------------------------------------- */
.quiz-field,
.quiz-other,
.quiz-text-answer {
  margin: 0 0 18px;
}

.quiz-field-label {
  display: block;
  font-size: 0.8125rem;
  font-weight: 700;
  color: var(--text);
  margin: 0 0 6px;
}

.quiz-field-optional {
  font-weight: 400;
  color: var(--text-secondary);
}

.quiz-input,
.quiz-textarea {
  width: 100%;
  padding: 12px 14px;
  min-height: 44px;
  border: 1px solid var(--field-border);
  border-radius: 10px;
  background: var(--field-bg);
  color: var(--text);
  font-family: inherit;
  font-size: 1rem;
  line-height: 1.5;
}
.quiz-input:focus-visible,
.quiz-textarea:focus-visible {
  outline: 3px solid var(--quiz-accent);
  outline-offset: 1px;
  border-color: var(--quiz-accent);
}

.quiz-textarea {
  resize: vertical;
  min-height: 120px;
}

.quiz-input.is-invalid {
  border-color: #D6453C;
}

.quiz-field-error {
  margin: 6px 0 0;
  font-size: 0.82rem;
  /* Not the theme's raw red on its own: this is small text on --card-bg, so
     it uses a shade that clears 4.5:1 in both themes rather than whichever
     red looked right in dark mode. */
  color: #C0392B;
}

[data-theme=dark] .quiz-field-error {
  color: #FF8B80;
}

.quiz-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 0 0 12px;
}

.quiz-chip {
  min-height: 36px;
  padding: 8px 14px;
  border-radius: 999px;
  border: 1px solid var(--chip-border);
  background: transparent;
  color: var(--text-secondary);
  font-family: inherit;
  font-size: 0.82rem;
  cursor: pointer;
}
.quiz-chip:hover {
  border-color: var(--quiz-accent);
  color: var(--text);
}
.quiz-chip:focus-visible {
  outline: 3px solid var(--quiz-accent);
  outline-offset: 2px;
}

/* --------------------------------------------------------------------------
   Buttons and navigation
   -------------------------------------------------------------------------- */
.quiz-nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-top: 24px;
  padding-top: 20px;
  border-top: 1px solid var(--border);
}

.quiz-btn {
  min-height: 48px;
  padding: 13px 26px;
  border-radius: 999px;
  border: 1px solid transparent;
  font-family: inherit;
  font-size: 0.95rem;
  font-weight: 700;
  cursor: pointer;
  transition: transform 140ms ease, background 140ms ease, opacity 140ms ease;
}
@media (prefers-reduced-motion: reduce) {
  .quiz-btn {
    transition: none;
  }
}
.quiz-btn:focus-visible {
  outline: 3px solid var(--quiz-accent);
  outline-offset: 3px;
}
.quiz-btn:disabled {
  opacity: 0.6;
  cursor: default;
}

.quiz-btn-primary {
  background: var(--quiz-accent);
  color: var(--quiz-accent-on);
}
.quiz-btn-primary:hover:not(:disabled) {
  transform: translateY(-1px);
  filter: brightness(1.06);
}

.quiz-btn-ghost {
  background: transparent;
  border-color: var(--border);
  color: var(--text-secondary);
}
.quiz-btn-ghost:hover {
  border-color: var(--quiz-accent);
  color: var(--text);
}

.quiz-limit-hint,
.quiz-limit-notice {
  margin: 0;
  font-size: 0.82rem;
  color: var(--text-secondary);
}

.quiz-limit-notice {
  color: var(--quiz-ink);
  font-weight: 700;
}

.quiz-submit-error {
  margin: 16px 0 0;
  padding: 12px 14px;
  border-radius: 10px;
  border: 1px solid #D6453C;
  background: rgba(214, 69, 60, 0.08);
  color: var(--text);
  font-size: 0.9rem;
}

/* --------------------------------------------------------------------------
   Reveal
   -------------------------------------------------------------------------- */
.quiz-shell-reveal .quiz-card {
  text-align: left;
}

.quiz-reveal-title {
  font-size: clamp(1.6rem, 5.2vw, 2.15rem);
}

.quiz-reveal-lead {
  font-size: 1.05rem;
}

.quiz-reveal-list {
  margin: 22px 0;
  padding: 20px 22px;
  border-radius: 14px;
  border: 1px solid var(--border);
  background: var(--field-bg);
}
.quiz-reveal-list ul {
  margin: 0;
  padding: 0;
  list-style: none;
}
.quiz-reveal-list li {
  position: relative;
  padding-left: 26px;
  margin: 0 0 10px;
  color: var(--text);
  line-height: 1.55;
}
.quiz-reveal-list li:last-child {
  margin-bottom: 0;
}
.quiz-reveal-list li {
  /* A drawn tick rather than a bullet glyph: it reads as "this is
     settled", which is the whole tone of this screen. */
}
.quiz-reveal-list li::before {
  content: "";
  position: absolute;
  left: 3px;
  top: 7px;
  width: 6px;
  height: 11px;
  border: solid var(--quiz-ink);
  border-width: 0 2px 2px 0;
  transform: rotate(45deg);
}

.quiz-reveal-list-intro {
  margin: 0 0 14px;
  font-weight: 700;
  color: var(--text);
}

.quiz-reveal-closing {
  margin: 0 0 6px;
  color: var(--text);
  font-weight: 700;
}

.quiz-reveal-contact {
  margin: 0 0 18px;
}
.quiz-reveal-contact a {
  color: var(--quiz-ink);
  font-weight: 700;
  text-decoration: none;
}
.quiz-reveal-contact a:hover {
  text-decoration: underline;
  text-underline-offset: 3px;
}

.quiz-reveal-signoff {
  margin: 0;
  color: var(--text-secondary);
  font-size: 0.9rem;
}

/* --------------------------------------------------------------------------
   Utilities
   -------------------------------------------------------------------------- */
.quiz-sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}
