/* ===========================================================
   styles.css — single stylesheet, no dependencies, nothing
   fetched over the network. System font stack throughout.
   There must never be a remote URL or a stylesheet import in
   this file. The deploy check greps for both and expects zero
   matches, so do not quote the grep pattern here either — a
   comment containing it fails the check it describes.
   =========================================================== */

/* ---- Reset -------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body, h1, h2, h3, h4, p, figure, blockquote, dl, dd, ul, ol { margin: 0; }
ul[class], ol[class] { list-style: none; padding: 0; }
img, picture, svg { max-width: 100%; display: block; }

/* An author `display` rule beats the UA stylesheet's [hidden]{display:none},
   so `.btn { display: inline-flex }` silently un-hides every element marked
   hidden. That matters here because the phone controls ship WITH the hidden
   attribute and are only revealed when SITE.phoneDisplay is set -- without
   this line a site with no phone number renders dead Call buttons. Found by
   review 2026-08-19. */
[hidden] { display: none !important; }
input, button, textarea, select { font: inherit; color: inherit; }
button { cursor: pointer; }
a { color: inherit; }

/* ---- Design tokens ------------------------------------------
   ONE palette, "Ridge" — cool paper ground, deep slate-blue for
   structure and links, one hi-vis amber reserved for the primary
   action and nothing else.

   LIGHT IS THE DEFAULT and it sits here on bare :root. That is a
   deliberate reversal of the previous stylesheet. The reader is
   over fifty, holding a phone at arm's length outdoors: bright
   ambient light constricts the pupil, which increases depth of
   field and sharpens the retinal image, and dark-on-light is what
   wins in that condition. A visitor who has stated no preference
   gets the light page. The dark variant is the exception and
   lives in the prefers-color-scheme: dark block underneath — it
   is a full redeclaration of the same token set, not a tint.

   Every pair below is CALCULATED, not eyeballed — sRGB relative
   luminance per WCAG 2.x. 4.5:1 for body text, 3:1 for large
   text and for any border that is a control edge. Ratios are
   against --color-bg unless stated.

   --color-border is the one value that does not clear 3:1 and
   must not be used as a control edge. It draws decorative
   dividers only (section hairlines, footer rules, table rows).
   Control edges are --color-border-strong. Do not swap them.
   ------------------------------------------------------------- */
:root {
  --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
    "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji",
    "Segoe UI Emoji";

  /* Small uppercase labels only (.eyebrow, footer/TOC headings, map
     labels). System monospace, never fetched. Reads like a line on a
     work order rather than a marketing kicker, which is the point. */
  --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
    "Liberation Mono", "Courier New", monospace;

  --color-bg: #eceef0;
  --color-bg-raised: #ffffff;
  --color-bg-sunken: #dde1e5;
  --color-text: #14161a;          /* 15.57:1 bg, 18.11 raised, 13.78 sunken, 13.79 band */
  --color-text-muted: #474e55;    /*  7.25:1 bg, 8.44 raised, 6.42 sunken, 6.42 band */
  --color-border: #b8bfc6;        /* dividers only — decorative, NOT a control edge */
  --color-border-strong: #717980; /*  3.80:1 bg, 4.42 raised, 3.36 sunken, 3.37 band */
  --color-accent: #2c4c60;        /*  7.82:1 bg, 9.09 raised, 6.92 sunken and band */
  --color-accent-hover: #1a3243;  /* 11.42:1 */
  --color-accent-contrast: #ffffff; /* 9.09:1 on accent */
  --color-accent-soft: #d8e2ea;
  /* The band is the full-bleed coloured field under the hero and the
     contact block. It is its own token so the two can be retuned
     apart from the accent chips; today they share a value. */
  --color-band: #d8e2ea;
  /* The map sits ON the band, so its ground cannot be --color-bg-raised in
     both schemes: in dark, #1c1f24 on a #1e2831 band is the same luminance
     and the valley outline disappears. Light goes up to paper, dark goes
     down to sunken, so the shape reads either way. Label contrast on it:
     text 18.11:1 light / 16.80:1 dark, muted 8.44 / 8.42, accent dots
     9.09 / 9.08, border-strong outline 4.42 / 3.85. */
  --color-map-ground: #ffffff;
  --color-action: #f5b52e;
  --color-action-hover: #dc9f14;
  --color-action-contrast: #141008; /* 10.41:1 on action, 8.14 on hover */
  --color-action-border: #6b4a00; /*  6.93:1 bg, 6.14 band. LOAD-BEARING: a bare
                                     #f5b52e fill is 1.57:1 against this page and
                                     has no boundary meeting the 3:1 a control
                                     needs. Do not delete this border. */
  --color-focus: #1259c9;         /*  5.47:1 bg, 4.85 band */
  --color-alert: #a3130b;         /*  6.80:1 — .todo only, must not share the CTA colour */
  --color-alert-soft: #fbe4e2;    /*  6.51:1 for alert-on-alert-soft */
  --color-success-bg: #dfeee3;
  --color-success-text: #1b5b2a;  /*  6.78:1 on success-bg */

  /* Hairline offsets, not blur. A soft ambient shadow is the single
     strongest "SaaS marketing page" tell; borders do the work here. */
  --shadow-sm: 0 1px 0 rgba(20, 22, 26, 0.07);
  --shadow-md: 0 2px 0 rgba(20, 22, 26, 0.08);

  --radius-sm: 3px;
  --radius-md: 4px;
  --radius-lg: 4px;

  --maxw: 1120px;

  /* The <select> chevron. A data: URI, which is what `img-src 'self'
     data:` in vercel.json permits — never a hosted image, and never an
     icon font. It cannot use currentColor, so the colour is baked in and
     the dark block below swaps the whole URI. Keep the two in step: an
     arrow that stays dark on the dark scheme disappears. */
  --select-arrow: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath fill='none' stroke='%23474e55' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round' d='M1 1.5 6 6.5 11 1.5'/%3E%3C/svg%3E");
  color-scheme: light;
}

@media (prefers-color-scheme: dark) {
  :root {
    --color-bg: #14161a;
    --color-bg-raised: #1c1f24;
    --color-bg-sunken: #0e1013;
    --color-text: #eef1f3;          /* 15.97:1 bg, 14.57 raised, 16.80 sunken, 13.19 band */
    --color-text-muted: #a7adb4;    /*  8.00:1 bg, 7.30 raised, 8.42 sunken, 6.61 band */
    --color-border: #2c3138;        /* dividers only */
    --color-border-strong: #69717a; /*  3.66:1 bg, 3.34 raised, 3.85 sunken, 3.02 band */
    --color-accent: #9fb6c9;        /*  8.63:1 bg, 7.13 on band/soft */
    --color-accent-hover: #bccfdf;
    --color-accent-contrast: #0b0f13; /* 9.17:1 on accent */
    --color-accent-soft: #1e2831;
    --color-band: #1e2831;
    --color-map-ground: #0e1013;
    --color-action: #f5b52e;        /*  9.95:1 — the fill's own edge clears 3:1 here,
                                       unlike the same amber on a light ground */
    --color-action-hover: #ffc850;
    --color-action-contrast: #141008; /* 10.41:1 on action, 12.31 on hover */
    --color-action-border: #f5b52e;
    --color-focus: #86bcff;         /*  9.19:1 bg, 7.59 band */
    --color-alert: #ff8a80;         /*  7.93:1 */
    --color-alert-soft: #3a1210;    /*  7.22:1 for alert-on-alert-soft */
    --color-success-bg: #10241a;
    --color-success-text: #8fd89b;  /*  9.67:1 on success-bg */
    --shadow-sm: 0 1px 0 rgba(0, 0, 0, 0.5);
    --shadow-md: 0 2px 0 rgba(0, 0, 0, 0.6);
    /* Same chevron, --color-text-muted for this scheme (#a7adb4). */
    --select-arrow: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath fill='none' stroke='%23a7adb4' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round' d='M1 1.5 6 6.5 11 1.5'/%3E%3C/svg%3E");
    color-scheme: dark;
  }
}

/* ---- Base -----------------------------------------------------
   112.5% = 18px root. Every size below is in rem, so this sets the
   whole page. 18px is the FLOOR, not the target — the reader is
   over fifty, holding a phone at arm's length in a truck. Nothing
   on this site may compute below 16px. Letter-spacing on body copy
   stays normal: extra tracking as a legibility aid is disproven.
   -webkit-font-smoothing: antialiased is deliberately NOT set — it
   thins glyph stems on macOS, which is the wrong trade here. */
/* scroll-padding-top is the sticky header's own height, kept off every
   anchor landing. Without it web.html#pricing, #contact, #terms, every
   .legal-toc link and the footer's own web.html#pricing all parked their
   target heading underneath the bar. Derived, not copied from the 5.5rem
   .legal-toc uses — that is sticky offset, a different job:

     .header-inner  padding-block 0.5rem x 2                    = 18.0px
     tallest item   .btn.header-cta at >= 900px, which is the
                    tallest the bar ever gets: 1rem x 1.6 line
                    box + 0.45rem padding x 2 + 2px border x 2  = 49.0px
     .site-header   border-bottom                               =  1.0px
                                                                  ------
                                                                  68.0px

   68px is 3.78rem (the compact bar is 63px, and .is-scrolled takes either
   down by ~4px), so 4.5rem clears the worst case with about 0.7rem of air
   left so the heading sits below the bar rather than against it. One value
   for both header sizes; a breakpoint here would buy nothing. */
html { scroll-behavior: smooth; scroll-padding-top: 4.5rem; font-size: 112.5%; }
body {
  font-family: var(--font-sans);
  background: var(--color-bg);
  color: var(--color-text);
  line-height: 1.6;
}

h1, h2, h3, h4 { line-height: 1.2; font-weight: 700; letter-spacing: 0; }
h1 { font-size: clamp(2rem, 1.5rem + 2.2vw, 3.1rem); }
h2 { font-size: clamp(1.5rem, 1.3rem + 1vw, 2.15rem); }
h3 { font-size: 1.2rem; }

/* Body copy is near-black, not mid-grey, and this is the single
   highest-value legibility decision on the page. Ambient light adds a
   veiling-glare floor to EVERY pixel, so in open daylight a 21:1 pair
   collapses to about 2.1:1 and a 7.5:1 pair to about 1.9:1 — everything
   lands near threshold and the only thing that survives is headroom.
   Setting body text to a "secondary" grey because it still clears AA
   indoors is exactly the wrong trade for a man reading this outdoors.
   --color-text-muted is kept, but only for genuine chrome: footnotes,
   footer, table cells, form hints, map labels. Hierarchy comes from
   size and weight here, not from fading the words out. */
p { color: var(--color-text); }
.lede { font-size: 1.2rem; color: var(--color-text); }

a { text-decoration-thickness: 1.5px; text-underline-offset: 2px; }
a:focus-visible,
button:focus-visible,
input:focus-visible,
textarea:focus-visible,
summary:focus-visible,
[tabindex]:focus-visible {
  outline: 3px solid var(--color-focus);
  /* The 2px offset puts the ring on the page background, not on the
     control, so the ring only has to clear 3:1 against --color-bg
     (5.47:1 light, 9.19:1 dark). That matters most on .btn-primary,
     where a blue ring drawn directly on the amber fill would fail.
     Do not remove the offset. */
  outline-offset: 2px;
  border-radius: 3px;
}

.container {
  width: 100%;
  max-width: var(--maxw);
  margin-inline: auto;
  padding-inline: 1.25rem;
}
@media (min-width: 640px) {
  .container { padding-inline: 2rem; }
}

/* Sections alternate ground, and every boundary also carries a
   hairline. The hairline is not redundant: in direct sun the two
   grounds converge visually and the band change stops registering,
   which is what turns a page into an undifferentiated column. */
.section { padding-block: 2.5rem; border-top: 1px solid var(--color-border); }
@media (min-width: 720px) { .section { padding-block: 3.5rem; } }
.section-alt { background: var(--color-bg-sunken); }

.eyebrow {
  display: inline-block;
  font-family: var(--font-mono);
  font-size: 0.9rem;              /* 16.2px — the floor. Do not reduce. */
  font-weight: 700;
  letter-spacing: 0;
  text-transform: uppercase;
  color: var(--color-accent);
  margin-bottom: 0.7rem;
}

.section-head { max-width: 40rem; margin-bottom: 2rem; }
.section-head p { margin-top: 0.6rem; }

/* ---- The mark ---------------------------------------------------
   "Ridge & River" — a foothills silhouette with the river cut
   through it as a single wavy slot. One path, fill-rule evenodd,
   fill=currentColor, so it inherits the page text colour in both
   schemes and can never fail contrast on its own. The same geometry
   is in assets/logo-mark.svg and assets/favicon.svg.
   ------------------------------------------------------------------ */
.mark { flex: none; color: var(--color-accent); }

/* ---- Header / brand lockup --------------------------------------
   Two lines beside the mark: the name, then the category in mono
   caps under it. This is the same brand-name / brand-category
   pattern the sites Austin ships to clients already use, which is
   the point — the storefront and the product should read as one
   shop. Both lines are filled from SITE.businessName by site.js
   (data-cfg-lockup); the text in the HTML is the no-JS fallback.
   ------------------------------------------------------------------ */
.site-header {
  position: sticky;
  top: 0;
  z-index: 40;
  /* Solid, not translucent. A blurred see-through bar is a phone-app
     affectation and it drops the contrast of whatever scrolls under it. */
  background: var(--color-bg);
  border-bottom: 1px solid var(--color-border-strong);
}
.header-inner {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding-block: 0.5rem;
}
.brand {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  text-decoration: none;
  margin-right: auto;
}
.brand .mark { width: 27px; height: 27px; }
.brand-text { display: block; line-height: 1.05; }
.brand-name {
  display: block;
  font-size: 1.05rem;
  font-weight: 800;
  letter-spacing: 0;
  text-transform: uppercase;
}
.brand-category {
  display: block;
  font-family: var(--font-mono);
  font-size: 0.68rem;
  font-weight: 600;
  /* Tracking here is a LOCKUP decision, not a legibility one: it optically
     justifies line two to the width of line one, which is what makes the
     block read as drawn rather than typed. The no-extra-tracking rule
     governs body copy, and this is a logo. */
  letter-spacing: 0;
  text-transform: uppercase;
  color: var(--color-text-muted);
  margin-top: 0.12rem;
}

/* The contact CTA lives in the header bar itself, NOT inside the
   collapsed nav. On a phone the old header was a name and a hamburger
   and nothing else, which left a buyer standing outdoors with no way
   to reach Austin without opening a menu. */
.btn.header-cta {
  min-height: 44px;              /* tap-target floor — do not go below 44 */
  padding: 0.35rem 0.75rem;
  font-size: 0.95rem;            /* 17.1px */
  border-width: 1px;
  white-space: nowrap;
  /* .btn sets flex-wrap: wrap further down, and "Call" and the digits are
     two flex items, so nowrap on the text alone did not stop the button
     stacking into two lines (87px tall) whenever the row ran short. */
  flex-wrap: nowrap;
}
/* .btn is defined LATER in this file, so an equal-specificity .header-cta
   loses every property it shares with it. Hence .btn.header-cta. If you move
   either block, keep the compound selector. */
@media (min-width: 900px) {
  .btn.header-cta { padding: 0.45rem 1.1rem; font-size: 1rem; border-width: 2px; }
}

/* ---- The compact header shows "Call", not the number ----------------
   Once js/config.js carries a real phone number, site.js unhides the
   header's Call button and it renders `Call&nbsp;<span>(208) 219-6714</span>`
   with white-space: nowrap. Measured at 375px, root 18px, system UI face:

     container content   375 - 2 x 1.25rem                = 330.0px
     .brand              27 mark + 0.55rem gap + 105.5 text = 142.4px
     header-inner gap    0.6rem                            =  10.8px
     .btn.header-cta     2 border + 27 padding + 35.1 "Call "
                         + 0.5rem .btn gap + 129.9 digits  = 203.0px
     header-inner gap    0.6rem                            =  10.8px
     .nav-toggle         flex: none                        =  44.0px
                                                             -------
                                                             411.0px

   81px over, and NOTHING can give: nowrap makes the button's min-content
   equal its max-content, .btn's overflow-wrap: anywhere cannot break a
   string with no break opportunity, the toggle is flex: none, and a flex
   item never shrinks below min-content. So the body scrolls sideways —
   the same failure the .btn comment further down documents fixing once.

   Taking the digits out of flow leaves 268.2px, 62px of room at 375px.

   WHY 67em, AND WHY THIS IS NOT THE 900 THAT THE NAV USES.
   The clip threshold used to be 900px because that is where the header
   changes composition -- nav inline, hamburger away. With seven links
   (Managed, 2026-09-11) the question is "where do the inline nav and the
   full-digit button fit", which is a larger number.

   Re-measured 2026-09-18 in Chrome at the 18px page root (112.5% of a 16px
   default), with the button held to one line:

     container padding   2 x 2rem                          =  72.0px
     .brand                                                = 141.4px
     header-inner gap    0.6rem x 2                        =  21.6px
     .site-nav           seven links, 1.6rem gaps          = 559.0px
     .site-nav margin    1.25rem                           =  22.5px
     .btn.header-cta     "Call (208) 219-6714", 2px border = 225.8px
                                                            --------
                                                            1042.3px

   plus a 15px classic scrollbar is 1058px. The 61em (976px) this replaced
   was too low: from 977px to 1042px the row ran short, .btn's flex-wrap
   let the button stack, and the header shipped with a two-line, 87px-tall
   Call button (seen at 1024px).

   The threshold is in em because a media-query em resolves against the
   BROWSER default font size, not against html { font-size: 112.5% }, while
   every width above scales with the page root. 1058 / 16 = 66.1em, rounded
   up to 67em (1072px) so the row is never flush. Below that the digits are
   clipped and the header fits from 900px; above it the full number shows.
   Re-measure this if a nav label, businessName or phoneDisplay changes.

   Below the threshold the header is the compact one and the button is an
   action, not a phone-number display — the number itself is still on the
   page at every one of these widths, in the sticky .mobile-cta, in
   #contact and in the footer.

   The digits are CLIPPED, not display: none, so they stay in the
   accessible name: the link still announces as "Call (208) 219-6714" and
   needs no aria-label. These are the declarations of the .visually-hidden
   utility, written out because the class cannot be added to the markup. */
@media (max-width: 67em) {
  .btn.header-cta [data-cfg-text="phoneDisplay"] {
    position: absolute;
    width: 1px; height: 1px;
    padding: 0; margin: -1px;
    overflow: hidden;
    clip: rect(0,0,0,0);
    border: 0;
  }
  /* The label is `Call&nbsp;`, and the nbsp is a preserved character, not
     collapsible white space — with the digits gone it is a 4px trailing
     space inside a justify-content: center button, which pushes the word
     off centre. word-spacing applies to U+00A0, so one negative space
     advance cancels it. 0.25em against a measured 0.2327em for the system
     face at 700; the residue either way is under half a pixel. It is safe
     because "Call" is the only word left in the button — if that label
     ever becomes two words, this has to go. */
  .btn.header-cta[data-phone-only] { word-spacing: -0.25em; }
}

.nav-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  flex: none;
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-sm);
  background: var(--color-bg-raised);
}
.nav-toggle svg { width: 22px; height: 22px; }
@media (min-width: 900px) {
  .nav-toggle { display: none; }
}

.site-nav {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  position: absolute;
  left: 0;
  right: 0;
  top: 100%;
  background: var(--color-bg-raised);
  border-bottom: 1px solid var(--color-border);
  padding: 0 1.25rem;
  max-height: 0;
  overflow: hidden;
  /* max-height alone does NOT collapse vertical padding or the border, which
     left an empty strip under the header when the menu was closed. Zero them
     here and restore on .is-open. visibility:hidden also keeps the collapsed
     links out of the keyboard tab order. */
  border-bottom-width: 0;
  visibility: hidden;
  transition: max-height 0.2s ease, padding 0.2s ease,
              border-bottom-width 0.2s ease, visibility 0s linear 0.2s;
}
.site-nav.is-open {
  /* Seven links since 2026-09-11 (Managed). Each is 28.8px of line plus
     0.55rem*2 padding and a 1px border, about 47px, plus the 0.25rem gaps
     and the open padding: ~365px in all. 320 clipped the last link. */
  max-height: 400px;
  padding: 0.5rem 1.25rem 1rem;
  border-bottom-width: 1px;
  visibility: visible;
  transition: max-height 0.2s ease, padding 0.2s ease,
              border-bottom-width 0.2s ease, visibility 0s;
}
.site-nav a {
  display: flex;
  align-items: center;
  padding: 0.7rem 0.25rem;
  min-height: 44px;
  text-decoration: none;
  font-weight: 600;
  border-bottom: 1px solid var(--color-border);
}
.site-nav a:last-child { border-bottom: none; }

/* ---- The nav says where you are -----------------------------------
   Every page has always marked its own link aria-current="page", and
   until 2026-09-18 nothing styled it: a screen reader was told which
   page this is and a sighted reader was not. site.js also sets
   aria-current="location" on Contact while #contact is on screen, so
   the selector is the bare attribute, not ="page" -- and it puts
   .is-away on the page link for that stretch, which hides the mark
   without taking aria-current="page" off a page that is still this page.

   Colour is never the only signal (WCAG 1.4.1): the current link also
   carries a bar -- down the left edge in the collapsed panel, under the
   word in the inline nav. Both are inset box-shadows, and the inline
   one adds no padding, so marking a link current changes no width and
   cannot re-open the header-width arithmetic further up. --color-accent is 9.09:1 on the raised panel
   and 7.82:1 on the header (8.63 dark). */
.site-nav a[aria-current]:not(.is-away) {
  color: var(--color-accent);
  box-shadow: inset 3px 0 0 var(--color-accent);
  padding-left: 0.8rem;
}
/* Windows High Contrast drops box-shadow and repaints link colour, which
   is both signals at once. An underline survives forced colours. */
@media (forced-colors: active) {
  .site-nav a[aria-current]:not(.is-away) {
    text-decoration: underline;
    text-decoration-thickness: 3px;
    text-underline-offset: 5px;
  }
}
@media (hover: hover) {
  .site-nav a:hover { color: var(--color-accent); }
}

@media (min-width: 900px) {
  /* desktop: nav is always shown, so undo every collapsed-state property */
  .site-nav, .site-nav.is-open {
    visibility: visible;
    max-height: none;
    border-bottom-width: 0;
    padding: 0;
    overflow: visible;
    transition: none;
  }
  .site-nav {
    position: static;
    flex-direction: row;
    align-items: center;
    background: transparent;
    border-bottom: none;
    gap: 1.6rem;
    margin-right: 1.25rem;
  }
  .site-nav a {
    border-bottom: none;
    min-height: auto;
    padding: 0.3rem 0;
  }
  .site-nav a[aria-current]:not(.is-away) {
    padding-left: 0;
    box-shadow: inset 0 -2px 0 var(--color-accent);
  }
}

/* ---- Buttons -------------------------------------------------- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  min-height: 48px;
  padding: 0.75rem 1.4rem;
  border-radius: var(--radius-sm);
  font-weight: 700;
  font-size: 1.05rem;
  border: 2px solid transparent;
  text-decoration: none;
  transition: background-color 0.15s ease, border-color 0.15s ease, transform 0.05s ease;
}
.btn:active { transform: translateY(1px); }
/* A button may never be wider than its column. An email address or a long
   label inside a full-width button is what made the body scroll sideways at
   375px; these three lines mean the worst case is an ugly wrap, not a page
   the reader has to drag around. */
.btn { max-width: 100%; flex-wrap: wrap; text-align: center; }
.btn span { overflow-wrap: anywhere; }

/* Amber is used here, on .skip-link, and on the sticky mobile bar. The
   only other amber on the site is the small button block inside the
   #proof wireframe, where its whole job is to say "this shape is the
   button" — it is 88 units wide inside a 200-unit diagram and it is
   never adjacent to a real control. That is the one exception, and it
   is the last one: the moment an ordinary page element is amber, the
   button stops being the obvious next move. */
.btn-primary {
  background: var(--color-action);
  color: var(--color-action-contrast);
  border-color: var(--color-action-border);
}
/* ---- Why every :hover below is guarded ------------------------------
   A touch browser fires :hover on tap and then LEAVES IT ON until the
   reader taps something else. Unguarded, every control on this site
   stays visibly pressed after it is used -- on a phone, which is the
   device this buyer evaluates the work on. It is invisible on a desktop
   mockup, which is why it survives so long.

   :active and :focus-visible stay OUTSIDE the guard. On touch :active is
   the only feedback channel there is, and a keyboard ring must never
   depend on a pointer capability. -------------------------------------- */
@media (hover: hover) {
  .btn-primary:hover { background: var(--color-action-hover); }
}

.btn-secondary {
  background: transparent;
  border-color: var(--color-border-strong);
  color: var(--color-text);
}
@media (hover: hover) {
  .btn-secondary:hover { border-color: var(--color-accent); color: var(--color-accent); }
}

.btn-block { width: 100%; }
.btn-row { display: flex; flex-wrap: wrap; gap: 0.75rem; }

/* ---- Icons ------------------------------------------------------
   One inline <symbol> sprite per page, defined once at the top of
   <body> and referenced with <use>. Line icons, never emoji: emoji
   render differently on every platform and read consumer-grade to
   this buyer. Stroke inherits the accent; nothing here is a control,
   so nothing here carries a contrast requirement on its own — each
   icon always sits beside the words it labels.
   ------------------------------------------------------------------ */
.ico {
  width: 24px;
  height: 24px;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.75;
  stroke-linecap: round;
  stroke-linejoin: round;
  color: var(--color-accent);
}

/* ---- Bands ------------------------------------------------------
   A full-bleed coloured field. This is the single device that makes
   the fold read as designed rather than as a default document, and it
   is used exactly twice: under the hero, and behind the contact block.
   ------------------------------------------------------------------ */
.band {
  background: var(--color-band);
  border-bottom: 1px solid var(--color-border-strong);
}

/* ---- Hero -------------------------------------------------------- */
.hero { padding-block: 2.5rem 3rem; }
.hero-grid { display: grid; gap: 2.25rem; align-items: center; }
@media (min-width: 900px) {
  .hero-grid { grid-template-columns: 1.05fr 0.95fr; gap: 3rem; }
}
.hero h1 { margin-bottom: 0.9rem; }
.hero .lede { margin-bottom: 1.5rem; max-width: 34ch; }
.hero-sig { margin-top: 1.1rem; color: var(--color-text-muted); }
.identity-panel {
  background: var(--color-bg-raised);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-md);
  padding: 1.5rem;
  box-shadow: var(--shadow-sm);
}
.identity-panel .identity-mark {
  width: 76px;
  height: 76px;
  margin-bottom: 1.2rem;
}
.identity-panel h2 { margin-bottom: 0.75rem; }

/* ---- The service-area map ---------------------------------------
   The one graphic on this site that is completely honest, completely
   specific, and that no competitor has: the nine towns, drawn.
   Geometry is a schematic, not a survey — the dots are placed from
   real relative latitude/longitude but the outline is indicative.

   The nine labels MUST match SITE.serviceAreaCities in js/config.js,
   which is also what feeds the JSON-LD areaServed. They are written
   into the HTML rather than generated so the map still draws with
   JavaScript off; site.js compares the two lists at runtime and warns
   in the console if they ever drift apart. If you add or remove a
   town, change config.js AND this markup together.
   ------------------------------------------------------------------ */
.map { width: 100%; height: auto; }
.map .ground { fill: var(--color-map-ground); stroke: var(--color-border-strong); stroke-width: 0.7; }
.map .river { fill: none; stroke: var(--color-accent); stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; opacity: 0.85; }
/* The dots are the accent, never the amber. Amber is the primary action
   and nothing else on this site is allowed to be amber — the moment a
   second thing is, the button stops being the obvious next move. Boise
   is emphasised with a larger radius instead of a second colour. */
.map .dot { fill: var(--color-accent); }
.map .lbl {
  font-family: var(--font-mono);
  font-size: 4.6px;
  font-weight: 700;
  fill: var(--color-text);
}
.map .lbl-soft { fill: var(--color-text-muted); font-weight: 400; font-size: 4px; }
.map-caption {
  font-family: var(--font-mono);
  font-size: 0.8rem;
  letter-spacing: 0;
  text-transform: uppercase;
  color: var(--color-text-muted);
  margin-top: 0.75rem;
}
/* The argument the map was already making silently. Body face, not the
   mono caption face: this is a sentence to read, not a label. */
.map-note {
  font-size: 0.95rem;
  color: var(--color-text-muted);
  margin-top: 0.6rem;
  max-width: 34em;
}

/* ---- Steps ------------------------------------------------------- */
.steps {
  counter-reset: step;
  display: grid;
  gap: 1.75rem;
}
@media (min-width: 720px) {
  .steps { grid-template-columns: repeat(3, 1fr); gap: 2rem; }
}
.step { position: relative; padding-top: 3.25rem; }
.step::before {
  counter-increment: step;
  content: counter(step);
  position: absolute;
  left: 0; top: 0;
  width: 2.5rem; height: 2.5rem;
  border-radius: 50%;
  background: var(--color-accent);
  color: var(--color-accent-contrast);
  display: flex; align-items: center; justify-content: center;
  font-weight: 800;
  font-size: 1.05rem;
}
.step h3 { margin-bottom: 0.4rem; }

/* ---- Cards ------------------------------------------------------- */
.grid { display: grid; gap: 1.5rem; }
@media (min-width: 640px) {
  .grid-2 { grid-template-columns: repeat(2, 1fr); }
  .grid-3 { grid-template-columns: repeat(3, 1fr); }
}
/* Three service cards on index.html carry a paragraph and a three-item list
   each; a 1/3 column at 640px is ~176px and the lists wrap to one word a
   line. Hold them two-up between 640px and 900px, where the header also goes
   inline and the container finally has width. The lower bound matters: below
   640px the base .grid sets no columns and the cards are one-up, and a rule
   without it puts two 164px cards on a 375px phone and overflows the page.
   The generated industry and city grids are unaffected: they are .card-link
   tiles with a heading and no list. */
@media (min-width: 640px) and (max-width: 899px) {
  #services .grid-3 { grid-template-columns: repeat(2, 1fr); }
}
/* The three service cards stretch to equal height, but their closing link
   lines did not agree: card 1's paragraph is a line shorter, so its link
   floated 28px above the other two. Push the link to the bottom of the card
   instead of padding the copy. */
.service-card { display: flex; flex-direction: column; }
.service-card > p.mt-md:last-child { margin-top: auto; }
/* Cards are defined by a real, visible edge rather than a soft drop
   shadow. --color-border-strong clears 3:1 against every surface it
   can sit on, which is what a phone screen in direct sun needs. */
.card {
  background: var(--color-bg-raised);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-md);
  padding: 1.5rem;
  box-shadow: var(--shadow-sm);
}
.card h3 { margin-bottom: 0.5rem; }
.card .ico { margin-bottom: 0.85rem; width: 30px; height: 30px; }

/* ---- The price --------------------------------------------------
   The price is an OBJECT, not a big heading: a bordered plate with the
   number, the unit, and the three terms that qualify it. Deliberately
   NOT a tier card and NOT a comparison grid — one price, no second
   plan, and no "most popular" badge, because with zero clients nothing
   is popular and a badge saying otherwise is fabricated social proof
   on a document signed "true and correct".
   ------------------------------------------------------------------ */
.price-plate {
  /* block + fit-content, NOT inline-block. As inline-block the plate shared a
     line box with the .eyebrow above it (which is also inline-block) and the
     two overlapped at desktop width, where the container is wide enough to fit
     both. fit-content keeps the shrink-to-fit box without the inline flow. */
  display: block;
  width: fit-content;
  max-width: 100%;
  border: 2px solid var(--color-border-strong);
  border-radius: var(--radius-md);
  background: var(--color-bg-raised);
  padding: 1.35rem 1.6rem;
  margin-bottom: 1.35rem;
}
.price-figure { display: flex; align-items: baseline; gap: 0.5rem; flex-wrap: wrap; }
.price-figure .amount {
  font-size: clamp(3.25rem, 2.4rem + 5.5vw, 5rem);
  font-weight: 800;
  line-height: 1;
  letter-spacing: 0;
}
.price-figure .unit { font-size: 1.1rem; font-weight: 600; color: var(--color-text-muted); }
.price-terms {
  font-family: var(--font-mono);
  font-size: 0.85rem;
  font-weight: 700;
  letter-spacing: 0;
  text-transform: uppercase;
  color: var(--color-text-muted);
  margin-top: 0.8rem;
  padding-top: 0.7rem;
  border-top: 1px solid var(--color-border);
}
.price-rule { font-size: 1.15rem; max-width: 44ch; margin-bottom: 2rem; }

/* ---- Feature list (icon rows) ------------------------------------ */
.feature-list { display: grid; gap: 1rem; }
@media (min-width: 720px) {
  .feature-list-2 { grid-template-columns: 1fr 1fr; gap: 1rem 2.5rem; }
}
.feature-list li { display: flex; gap: 0.8rem; align-items: flex-start; }
.feature-list li .ico { margin-top: 0.15rem; }
.feature-list li strong { display: block; }
.feature-list li > div { max-width: 58ch; }

.plan-cols { display: grid; gap: 2rem; margin-bottom: 2rem; }
@media (min-width: 720px) {
  .plan-cols { grid-template-columns: 1fr 1fr; gap: 2.5rem; }
}
.plan-col h3 {
  padding-bottom: 0.55rem;
  border-bottom: 2px solid var(--color-accent);
  margin-bottom: 1rem;
}

/* ---- Comparison table -------------------------------------------
   Two columns, both of them Austin's own categories. This is not a
   competitor comparison and must never become one.
   ------------------------------------------------------------------ */
.compare {
  width: 100%;
  border-collapse: collapse;
  margin-bottom: 1rem;
}
.compare caption {
  text-align: left;
  caption-side: bottom;
  padding-top: 0.9rem;
  color: var(--color-text);
  max-width: 62ch;
}
.compare th {
  text-align: left;
  padding: 0 0 0.6rem;
  font-size: 1rem;
  border-bottom: 2px solid var(--color-border-strong);
  vertical-align: bottom;
}
.compare th:first-child { padding-right: 0.75rem; }
.compare td {
  padding: 0.5rem 0.6rem 0.5rem 1.35rem;
  border-bottom: 1px solid var(--color-border);
  vertical-align: top;
  position: relative;
  width: 50%;
}
.compare td:empty { border-bottom-color: transparent; }
.compare td::before {
  position: absolute;
  left: 0;
  font-weight: 800;
}
.compare td:empty::before { content: none; }
.compare .col-in::before { content: "\2713"; color: var(--color-success-text); }
.compare .col-add::before { content: "+"; color: var(--color-accent); }
.compare .hdr-in { color: var(--color-success-text); }
.compare .hdr-add { color: var(--color-accent); }

/* ---- Disclosure footnotes ---------------------------------------
   The four long price footnotes live in <details> so the section reads
   as a page rather than an essay. Every word inside is unchanged and
   every one of them is still in #pricing, which legal/disclosures.md
   Block M requires. site.js forces every <details> open on beforeprint
   so the Attorney General exhibit prints complete — do not remove that
   handler, and do not move any of this content off the home page.
   ------------------------------------------------------------------ */
.faq-item { border-bottom: 1px solid var(--color-border); }
.faq-item summary {
  cursor: pointer;
  list-style: none;
  padding: 1.05rem 0;
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  min-height: 44px;
}
.faq-item summary::-webkit-details-marker { display: none; }
.faq-item summary .chev { flex: none; transition: transform 0.15s ease; color: var(--color-accent); }
.faq-item[open] summary .chev { transform: rotate(180deg); }
.faq-item p { padding-bottom: 1.05rem; max-width: 64ch; }
.faq-item p + p { padding-bottom: 1.05rem; margin-top: -0.3rem; }

/* ---- The wireframe ----------------------------------------------
   An abstract diagram of the page shape a buyer gets. It contains no
   text, no business name, no screenshot and no link to anything in
   sites/ — there is no consent to show a real client's page and there
   are no clients to have one.
   ------------------------------------------------------------------ */
.wire { width: 100%; max-width: 175px; height: auto; }
@media (min-width: 760px) { .wire { max-width: 230px; } }
.wire .frame { fill: var(--color-bg-raised); stroke: var(--color-border-strong); stroke-width: 2; }
.wire .blk { fill: var(--color-accent-soft); }
.wire .blk-strong { fill: var(--color-accent); }
.wire .blk-action { fill: var(--color-action); stroke: var(--color-action-border); stroke-width: 1; }
.wire .ln { stroke: var(--color-border-strong); stroke-width: 2; stroke-linecap: round; }
.wire-caption {
  font-family: var(--font-mono);
  font-size: 0.8rem;
  letter-spacing: 0;
  text-transform: uppercase;
  color: var(--color-text-muted);
  margin-top: 0.85rem;
}
.proof-grid { display: grid; gap: 2rem; align-items: start; }
@media (min-width: 760px) {
  .proof-grid { grid-template-columns: 1fr auto; gap: 3rem; }
}

/* ---- Who ---------------------------------------------------------- */
.who-head { display: flex; align-items: center; gap: 1.1rem; margin-bottom: 1.25rem; }
.who-head .mark { width: 56px; height: 56px; }

/* ---- Contact ------------------------------------------------------ */
/* minmax(0, ...) and min-width: 0 for the same reason as .legal-layout: a
   bare 1fr track cannot go below its min-content, and the min-content here
   is austin@collistercomputing.com, one unbreakable 300px word. At 320px
   -- the WCAG 1.4.10 reflow width, and any phone with the text size turned
   up -- that pushed #contact 36px past the screen. overflow-wrap lets the
   address break only when it has to; at 360px and up it never does. */
.contact-grid { display: grid; gap: 2.5rem; grid-template-columns: minmax(0, 1fr); }
.contact-grid > * { min-width: 0; }
/* break-word, NOT anywhere: `anywhere` also shrinks the word's min-content
   contribution, which stops the footer's fr columns growing to keep the
   address on one line at 640px and up. */
.contact-address a, .footer-grid a { overflow-wrap: break-word; }
@media (min-width: 860px) {
  .contact-grid { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 3rem; }
}
/* The CTA needs air more than anything else on the page: a crowded
   list of controls costs a reader roughly twice the time to work
   through. Full width below 640px, generous gaps, 56px tall. */
.contact-primary .btn { min-height: 56px; }
.contact-primary .btn-row { flex-direction: column; align-items: stretch; gap: 1rem; }
@media (min-width: 640px) {
  .contact-primary .btn-row { flex-direction: row; align-items: center; flex-wrap: wrap; }
  .contact-primary .btn { min-width: 15rem; }
}

/* The address under the contact buttons: a real link, selectable and
   tappable, allowed to break mid-string so it cannot widen the page. */
.contact-address a {
  font-weight: 700;
  overflow-wrap: anywhere;
  color: var(--color-accent);
}

.field { display: flex; flex-direction: column; gap: 0.35rem; margin-bottom: 1rem; }
.field label { font-weight: 700; font-size: 0.95rem; }
.field input,
.field select,
.field textarea {
  /* A form field's border IS the control — WCAG 1.4.11 applies, so this
     is --color-border-strong and never --color-border. */
  border: 1px solid var(--color-border-strong);
  background: var(--color-bg-raised);
  border-radius: var(--radius-sm);
  padding: 0.65rem 0.8rem;
  min-height: 48px;
  /* Inherit rather than take the UA default. A <select> that renders in
     Arial next to inputs that render in the body face reads as a widget
     bolted on afterwards, and on a dark scheme a UA-default select is
     light-on-light and effectively unreadable — the colour has to be
     stated, not assumed. */
  font: inherit;
  color: var(--color-text);
}
.field select {
  /* WebKit ignores min-height and most padding on a menulist select, so
     the two dropdowns rendered about half the height of the inputs
     beside them and well under the 48px touch target this buyer needs on
     a phone. Dropping the native appearance is what makes the box
     obey — which means drawing the chevron back on by hand, because a
     select with no arrow does not read as a select. */
  appearance: none;
  -webkit-appearance: none;
  padding-right: 2.4rem;
  background-image: var(--select-arrow);
  background-repeat: no-repeat;
  background-position: right 0.85rem center;
  background-size: 12px 8px;
}
.field textarea { min-height: 110px; resize: vertical; }
.form-note { font-size: 0.95rem; color: var(--color-text-muted); margin-top: 0.75rem; }

/* Result of a contact-form submission, written by js/site.js. role="status"
   + aria-live on the element, so the text is announced when it appears; it
   ships with the hidden attribute and stays empty until there is a result.
   Success and alert pairs are the tokens already rated in the palette block
   (6.78:1 / 6.51:1 light, 9.67:1 / 7.22:1 dark). */
.form-status {
  margin-top: 0.75rem;
  padding: 0.75rem 0.9rem;
  border-radius: var(--radius-sm);
  font-size: 0.95rem;
  line-height: 1.5;
}
.form-status.is-success { background: var(--color-success-bg); color: var(--color-success-text); }
.form-status.is-error { background: var(--color-alert-soft); color: var(--color-alert); }
.form-status a { color: inherit; font-weight: 700; overflow-wrap: anywhere; }

/* Honeypot wrapper: moved off-screen rather than display:none, so a bot that
   fills every input still fills `company` and the Worker drops the post.
   aria-hidden and tabindex="-1" in the markup keep it out of the reading
   and tab order. Same technique as .cf-hp in the site template partial. */
.form-hp {
  position: absolute !important;
  left: -9999px !important;
  width: 1px !important;
  height: 1px !important;
  overflow: hidden;
}

/* ---- Sticky mobile action bar ------------------------------------
   This is the slot the "PREVIEW ONLY — PICK A LOOK" debug toolbar was
   squatting in, half-burying the hero's own Email button in every
   screenshot Austin ever saw. It now carries the actual primary
   action. Mobile only, appears once the hero has scrolled out, hides
   again over #contact so it is never doubled up with the real buttons.
   No slide, no fade under prefers-reduced-motion.
   ------------------------------------------------------------------ */
.mobile-cta {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  z-index: 50;
  background: var(--color-bg);
  border-top: 1px solid var(--color-border-strong);
  padding: 0.6rem 1rem calc(0.6rem + env(safe-area-inset-bottom, 0px));
  transform: translateY(110%);
  transition: transform 0.2s ease;
}
.mobile-cta.is-visible { transform: translateY(0); }
.mobile-cta .btn { width: 100%; min-height: 52px; }
@media (min-width: 768px) {
  .mobile-cta { display: none; }
}
@media (prefers-reduced-motion: reduce) {
  .mobile-cta { transition: none; }
}
/* Keeps the bar from covering the last line of the footer. */
body.has-mobile-cta { padding-bottom: 0; }
@media (max-width: 767px) {
  body.has-mobile-cta .site-footer { padding-bottom: 6rem; }
}

/* ---- Unfilled facts -------------------------------------------------
   .todo marks a fact Austin has not supplied yet. It is deliberately
   ugly and deliberately visible: this site is Exhibit 4 of a state
   filing, and a bracket that quietly looks like body copy is how a
   placeholder ships. Nothing carrying this class may be live or printed
   for the Attorney General. Grep for class="todo" before deploying —
   the count must be zero.
   Red, never the accent: with the accent also carrying every ordinary
   link, a placeholder tinted with it would stop looking like a defect,
   which is the one thing this class exists to prevent. 6.51:1 on
   --color-alert-soft. Red appears nowhere else on the site.
   -------------------------------------------------------------------- */
.todo {
  font-family: var(--font-mono);
  font-size: 0.9em;
  font-weight: 700;
  background: var(--color-alert-soft);
  color: var(--color-alert);
  border: 1px dashed var(--color-alert);
  border-radius: 3px;
  padding: 0.05em 0.4em;
  white-space: nowrap;
}

.notice {
  border-left: 4px solid var(--color-accent);
  background: var(--color-accent-soft);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  padding: 1rem 1.25rem;
}
.notice strong { display: block; margin-bottom: 0.25rem; }

/* ---- Prose --------------------------------------------------------- */
.prose > * + * { margin-top: 1.05rem; }
.prose p, .prose li { max-width: 64ch; }
.prose-list { display: flex; flex-direction: column; gap: 1.05rem; }
/* NO display:block on strong here. :first-child does not do what you want —
   text nodes are not children for that selector, so <li>The quote gives you
   <strong>both numbers</strong>: …</li> matched it and the sentence split
   across three lines. Lead-in bolds that DO get their own line live in
   .feature-list, where the strong genuinely opens the item. */

/* The add-on list beside "What $99 a month buys". Marked with the same "+"
   the add-on column of the comparison table uses, so the two say the same
   thing in the same visual language. */
.plan-col .prose-list li { position: relative; padding-left: 1.4rem; }
.plan-col .prose-list li::before {
  content: "+";
  position: absolute;
  left: 0;
  font-weight: 800;
  color: var(--color-accent);
}
.rule-box { border-left: 4px solid var(--color-border); padding: 0.25rem 0 0.25rem 1.1rem; }

/* ---- Footer ----------------------------------------------------------- */
.site-footer {
  border-top: 1px solid var(--color-border-strong);
  background: var(--color-bg-sunken);
  padding-block: 2.5rem;
}
.footer-brand { display: flex; align-items: center; gap: 0.6rem; margin-bottom: 0.75rem; }
.footer-brand .mark { width: 26px; height: 26px; }
/* One column on a phone: minmax(0, 1fr) plus min-width: 0, so the email
   address breaks instead of pushing the page 9px wide at 320px. */
.footer-grid { display: grid; gap: 1.75rem; grid-template-columns: minmax(0, 1fr); }
@media (max-width: 719.98px) {
  .footer-grid > * { min-width: 0; }
}
@media (min-width: 720px) {
  /* Bare fr and NO min-width: 0 on purpose: here the Contact column SHOULD
     grow past its 1fr share to keep the email address on one line, and
     there is room for it to. 720, not the 640 it was: three columns need
     630px of content on the legal pages (support@ is the longer address)
     and the container has 595px at 640, so from 640 to 665 every page
     scrolled 12-26px sideways. */
  .footer-grid { grid-template-columns: 2fr 1fr 1fr; }
}
.footer-grid h4 {
  font-family: var(--font-mono);
  font-size: 0.9rem;
  text-transform: uppercase;
  letter-spacing: 0;
  margin-bottom: 0.75rem;
}
.footer-brand h4 { margin-bottom: 0; }
.footer-grid ul { display: flex; flex-direction: column; gap: 0.5rem; }
.footer-grid a { text-decoration: none; color: var(--color-text-muted); }
@media (hover: hover) {
  .footer-grid a:hover { color: var(--color-accent-hover); }
}
.footer-bottom {
  margin-top: 2rem;
  padding-top: 1.25rem;
  border-top: 1px solid var(--color-border);
  font-size: 0.95rem;
  color: var(--color-text-muted);
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1.5rem;
  justify-content: space-between;
}

/* ---- Legal page -------------------------------------------------------- */
/* minmax(0,1fr) not 1fr, and min-width: 0 on the items: a grid track's
   default min-width is auto, so the 576px .data-table forced the track wider
   than the phone instead of scrolling inside its own .table-wrap. Both
   guards sat INSIDE the 960px query until 2026-09-18, which is the one
   width range where the phone they were written for never sees them:
   legal.html scrolled 223px sideways at 375px. */
.legal-layout { display: grid; gap: 2.5rem; grid-template-columns: minmax(0, 1fr); }
.legal-layout > * { min-width: 0; }
@media (min-width: 960px) {
  .legal-layout { grid-template-columns: 220px minmax(0, 1fr); align-items: start; }
  .legal-toc { position: sticky; top: 5.5rem; }
}
.legal-toc h2 {
  font-family: var(--font-mono);
  font-size: 0.9rem;
  text-transform: uppercase;
  letter-spacing: 0;
  margin-bottom: 0.75rem;
}
.legal-toc ul { list-style: none; padding: 0; display: flex; flex-direction: column; gap: 0.45rem; }
.legal-toc a { color: var(--color-text-muted); text-decoration: none; font-size: 1rem; }
@media (hover: hover) {
  .legal-toc a:hover { color: var(--color-accent-hover); text-decoration: underline; }
}

.legal-section { padding-top: 2.25rem; }
.legal-section:first-child { padding-top: 0; }
.legal-section + .legal-section { border-top: 1px solid var(--color-border); }
.legal-section h2 { margin-bottom: 1rem; }
.legal-section h3 { margin-top: 1.75rem; margin-bottom: 0.5rem; font-size: 1.05rem; }
.legal-section p, .legal-section li { max-width: 66ch; }
/* Paragraph rhythm. The global reset zeroes every p margin, and this
   section never put any back — so consecutive paragraphs across the whole
   legal page ran together as one block with no visible break. On a page
   that IS long-form prose about money and cancellation terms, that is the
   difference between readable and a wall. Set on the flow, not on p
   alone, so lists separate from prose too. */
.legal-section p + p,
.legal-section p + ul,
.legal-section p + ol,
.legal-section ul + p,
.legal-section ol + p,
.legal-section p + .terms-note,
.legal-section ul + h4 { margin-top: 1rem; }
.legal-section ul:not([class]) {
  list-style: disc;
  padding-left: 1.25rem;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  color: var(--color-text);
}
.legal-section ul:not([class]) li { max-width: 64ch; }
.legal-meta { font-size: 0.95rem; color: var(--color-text-muted); }

/* The statutory Notice of Cancellation. Marked up as a bordered
   specimen, never as body copy: a visitor reading this page is NOT in a
   three-day cancellation window, and the framing has to make that
   obvious. The quoted sentence inside is verbatim from
   src/outreach/templates/notice_of_cancellation.txt and may not be
   reworded, shortened, or "made friendlier". */
.specimen {
  border: 2px solid var(--color-border-strong);
  border-radius: var(--radius-md);
  background: var(--color-bg-raised);
  padding: 1.5rem;
  margin: 0;
}
.specimen h3 {
  margin-top: 0;
  font-family: var(--font-mono);
  font-size: 1rem;
  letter-spacing: 0;
  text-transform: uppercase;
}
.specimen .statute {
  border-left: 4px solid var(--color-accent);
  padding-left: 1rem;
  margin: 1.25rem 0;
  font-weight: 700;
  color: var(--color-text);
}
.specimen .blank {
  display: inline-block;
  min-width: 14rem;
  border-bottom: 1px solid var(--color-text-muted);
  vertical-align: baseline;
}
.specimen .sig {
  margin-top: 1.25rem;
  padding-top: 1rem;
  border-top: 1px solid var(--color-border);
  font-size: 1rem;
}

/* ---- Data table (billing ladder on legal.html) ------------------------- */
.table-wrap { overflow-x: auto; }
.data-table {
  border-collapse: collapse;
  width: 100%;
  min-width: 32rem;
  font-size: 1rem;
}
.data-table th, .data-table td {
  text-align: left;
  padding: 0.7rem 0.9rem;
  border-bottom: 1px solid var(--color-border);
  vertical-align: top;
}
.data-table th { font-weight: 700; }
.data-table td { color: var(--color-text-muted); }

/* ---- Utility ------------------------------------------------------------ */
.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0,0,0,0);
  white-space: nowrap;
  border: 0;
}
.skip-link {
  position: absolute;
  left: 1rem; top: -100px;
  background: var(--color-action);
  color: var(--color-action-contrast);
  border: 2px solid var(--color-action-border);
  padding: 0.6rem 1rem;
  border-radius: var(--radius-sm);
  z-index: 100;
  transition: top 0.15s ease;
}
.skip-link:focus { top: 1rem; }

.text-center { text-align: center; }
.mt-lg { margin-top: 2rem; }
.mt-md { margin-top: 1.25rem; }
.measure { max-width: 64ch; }
.small { font-size: 0.95rem; }

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

/* ---- Print --------------------------------------------------------------
   This site is Exhibit 4 of an Idaho Attorney General telephone solicitor
   registration. Someone will print index.html, about.html and legal.html to
   PDF at desktop width and mail them. Make that readable: drop the sticky
   header, the nav and the sticky action bar, force black ink on white, keep
   the footer (it carries the identity line the exhibit needs), and print the
   destination of every link so a paper reviewer can follow it.

   The mark prints too — it is a solid black silhouette, which is exactly
   what survives a laser printer.
   -------------------------------------------------------------------------- */
@media print {
  :root {
    --color-bg: #ffffff;
    --color-bg-raised: #ffffff;
    --color-bg-sunken: #ffffff;
    --color-band: #ffffff;
    --color-map-ground: #ffffff;
    --color-text: #000000;
    --color-text-muted: #222222;
    --color-border: #999999;
    --color-border-strong: #666666;
    --color-accent: #000000;
    --color-accent-hover: #000000;
    --color-accent-contrast: #ffffff;
    --color-accent-soft: #ffffff;
    /* Amber prints as a pale nothing on a laser printer and the AG
       reviewer is reading toner, not pixels. Flatten it like everything
       else. */
    --color-action: #ffffff;
    --color-action-hover: #ffffff;
    --color-action-contrast: #000000;
    --color-action-border: #000000;
    --color-alert: #000000;
    --color-alert-soft: #ffffff;
    --color-focus: #000000;
    --color-success-bg: #ffffff;
    --color-success-text: #000000;
    --shadow-sm: none;
    --shadow-md: none;
    color-scheme: light;
  }
  .site-header, .skip-link, .nav-toggle, .site-nav, .mobile-cta { display: none !important; }

  /* Freeze the hero at its finished state.
     The map plots itself over ~2.4 seconds on load. A print fired a beat
     into that sequence would otherwise capture a half-drawn valley and
     towns that had not arrived yet -- the same class of bug the
     beforeprint handler in js/site.js already guards against for the
     collapsed <details>, just smaller and, until now, unguarded.
     The ground is decoration and wastes toner; it is hidden below. */
  .quad-edge, .quad-river, .quad-river-wide,
  .quad-towns g, .quad-collar, .quad-note {
    animation: none !important;
    opacity: 1 !important;
    stroke-dashoffset: 0 !important;
  }
  .quad-flow { display: none !important; }
  .site-footer { border-top: 1px solid #999; margin-top: 1.5rem; padding-bottom: 0 !important; }
  body { background: #fff; color: #000; font-size: 11pt; }
  .section, .hero { padding-block: 1.25rem; }
  .band { background: #fff; }
  .card, .specimen, .notice, .price-plate {
    border: 1px solid #999 !important;
    box-shadow: none !important;
    break-inside: avoid;
  }
  /* The price footnotes and the FAQ answers are inside <details>. CSS
     cannot force those open, so site.js opens every one on beforeprint —
     see the print handler there. A printout with them collapsed is a
     printout that omits the Block M disclosures. This just keeps them
     from being clipped. */
  .faq-item { break-inside: avoid; }
  .faq-item summary .chev { display: none; }
  .steps { break-inside: avoid; }
  .legal-toc { display: none; }
  .legal-layout { display: block; }
  a[href^="http"]::after,
  a[href^="mailto"]::after { content: " (" attr(href) ")"; font-size: 0.85em; }
  h1, h2, h3 { break-after: avoid; }
}

/* ===================================================================
   THE QUADRANGLE — hero graphic on index.html and apps.html
   ===================================================================
   The SVG is generated by tools/make-ridge.py and written into both
   pages between RIDGE:BEGIN / RIDGE:END markers. It is a contour map of
   the same nine towns and the same river that web.html's hero map
   draws, so the two graphics describe one valley.

   WHY EVERY COLOUR HERE IS A TOKEN AND NOT A HEX.
   The site has a full light/dark token system and this graphic has to
   work in both. It is not the same picture dimmed: on light it is ink
   on paper — a printed survey sheet — and on dark it is a lit readout,
   with fewer visible lines because the same alpha reads louder against
   a dark ground. The two are tuned separately below.

   NO BLUR, ANYWHERE. Not filter: blur(), not backdrop-filter, not a
   glow. Real survey drawings do not glow, blur is the single most
   expensive thing you can put on a phone GPU, and it is the difference
   between this reading as an instrument and reading as a screensaver.

   AMBER IS WATER ONLY. --color-action is the CTA colour. If it appears
   anywhere in this graphic except the river, the Call button stops
   being the brightest thing on the page and the hierarchy breaks.
   Boise is the largest dot, not the amber one.
   ------------------------------------------------------------------- */
.quad-panel { display: flex; flex-direction: column; gap: 0.9rem; }
.quad { width: 100%; height: auto; display: block; overflow: visible; }

.iso {
  fill: none;
  stroke: var(--color-accent);
  stroke-width: 0.19;
  opacity: 0.40;
  vector-effect: non-scaling-stroke;
}
.iso-index { stroke-width: 0.34; opacity: 0.58; }

.quad-edge {
  fill: none;
  stroke: var(--color-border-strong);
  stroke-width: 0.35;
  opacity: 0.6;
}

/* The river. Two strokes, not a blur: a wide low-alpha body under a
   thin bright line. Same visual result as a glow, none of the cost. */
.quad-river-wide {
  fill: none;
  stroke: var(--color-action);
  stroke-width: 2.0;
  opacity: 0.10;
  stroke-linecap: round;
}
.quad-river {
  fill: none;
  stroke: var(--color-action);
  stroke-width: 0.5;
  opacity: 0.85;
  stroke-linecap: round;
}

.quad-dot { fill: var(--color-text-muted); }
.quad-dot-lg { fill: var(--color-text); }
.quad-lbl {
  fill: var(--color-text-muted);
  font-family: var(--font-mono);
  font-size: 2.8px;
  letter-spacing: 0.08px;
}
.quad-collar {
  fill: var(--color-border-strong);
  font-family: var(--font-mono);
  font-size: 2.3px;
  letter-spacing: 0.34px;
}
.quad-note {
  font-size: 0.92rem;
  color: var(--color-text-muted);
  margin: 0;
  max-width: 34em;
}

@media (prefers-color-scheme: dark) {
  /* Same alpha reads louder on a dark ground, so pull it back rather
     than inheriting the light values. */
  .iso { opacity: 0.30; }
  .iso-index { opacity: 0.46; }
  .quad-edge { opacity: 0.45; }
  .quad-river-wide { opacity: 0.09; }
}

/* Phones: the labels are the point, so they get bigger while the
   contours get quieter. Nine names at 2.8px on a 375px screen are
   illegible, which would make this decoration instead of information. */
@media (max-width: 620px) {
  .quad-lbl { font-size: 3.6px; }
  .quad-collar { font-size: 2.9px; }
  .iso { opacity: 0.26; stroke-width: 0.24; }
  .iso-index { opacity: 0.42; }
}

/* ---- The mark layer -----------------------------------------------
   js/quad.js draws sparse monospace marks drifting down the river on
   this canvas. It ships hidden and is revealed only after the script
   has checked reduced-motion, visibility and device tier — so a
   browser with JavaScript off never gets an empty box sitting on top
   of the map, and the map alone is a finished graphic.
   ------------------------------------------------------------------- */
.quad-wrap { position: relative; }
.quad-flow {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}
.quad-flow[hidden] { display: none; }

/* ===================================================================
   THE PAGE GROUND — index.html only
   ===================================================================
   A contour field behind the entire page. Same noise function as the
   hero map, different crop, generated by tools/make-ridge.py into the
   GROUND:BEGIN / GROUND:END block.

   THREE RULES, ALL LOAD-BEARING.

   1. It lives BEHIND the content, always. z-index -1, and nothing else
      on the page is promoted to sit above it. It is a child of <body>,
      neither <body> nor <html> creates a stacking context, and the body
      background propagates to the canvas rather than painting as a box
      of its own — so a negative index resolves against the root and
      lands under every in-flow element on the page and above the page
      colour, which is exactly the layer wanted. Every colour pair on
      this site carries a calculated WCAG ratio in the token block
      above; putting this over the text would invalidate every one of
      them at once for a decorative flourish. Never raise it, and never
      give <body> a transform, filter, opacity, isolation or
      will-change: those all create a stacking context that would trap
      this layer behind the page colour and make it disappear.
   2. It is quieter than the body text by an order of magnitude. The
      target is that you notice the page has a texture without ever
      resolving what it is while reading. If you can read the contours,
      it is too loud.
   3. Transform only. The drift is one translate3d keyframe, which the
      compositor runs off the main thread. No opacity animation, no
      filter, no background-position — those all repaint.
   ------------------------------------------------------------------- */
.page-ground {
  position: fixed;
  /* Overscan has to exceed the drift distance below, or the turn exposes
     an edge. 18% of a short laptop viewport is ~150px against 74px of
     vertical travel. If you increase the travel, increase this first. */
  inset: -18% -12%;
  z-index: -1;
  pointer-events: none;
  /* Promote once. One element, one layer, for the life of the page. */
  will-change: transform;
  /* LINEAR, not ease-in-out. An eased drift spends most of its time
     near the endpoints crawling at a fraction of the average speed,
     and the first version of this -- 26px over 100s, eased -- was
     measurably running and completely invisible. Constant velocity at
     ~2px/sec is the slowest a low-contrast texture can move and still
     be perceptible. Below that you are paying for an animation nobody
     can see; above roughly 4px/sec it starts pulling the eye off the
     copy.

     Doubled from 72s to 36s on 2026-08-23 at Austin's request. 10vw
     over 36s is ~4px/sec on a 1456px laptop -- the top of that band,
     deliberately, not past it. If it ever needs to go faster than this,
     reduce the TRAVEL rather than the duration: the travel has to stay
     inside the overscan above or the turn exposes an edge, and speeding
     it up further starts competing with the headline for attention.

     On a phone the absolute speed drops, but so does the rendered size
     of the pattern, so the motion reads the same relative to what you
     can actually see. */
  animation: ground-drift 36s linear infinite alternate;
}
.page-ground-svg {
  width: 100%;
  height: 100%;
  display: block;
  fill: none;
  stroke: var(--color-accent);
  stroke-width: 0.55;
  opacity: 0.05;
}

/* Viewport units, NOT pixels, and the reason matters.
   .page-ground is position: fixed, so its inset percentages resolve
   against the viewport: inset -18% -12% gives 12vw of horizontal
   overscan on each side and 18vh vertical. A fixed-pixel drift blows
   through that on a narrow screen -- 148px of travel against 12% of a
   375px phone (45px) exposes a hard edge mid-animation. In vw/vh the
   travel stays inside the overscan at every size, by construction.
   Keep travel < overscan: 10 < 12 and 6 < 18. */
@keyframes ground-drift {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(-10vw, -6vh, 0); }
}

/* NOTHING IS PROMOTED ABOVE THE GROUND, and it must stay that way.
   REMOVED 2026-08-25, do not put it back:

     .has-ground .site-header, .has-ground #main, .has-ground .site-footer,
     .has-ground .skip-link, .has-ground .mobile-bar
       { position: relative; z-index: 1; }

   One rule, three defects, all of them only on index.html because that is
   the only page carrying .has-ground.

   At (0,2,0) it outranked `.site-header { position: sticky }` above and sat
   later in the file, so the header scrolled away and never came back — which
   also made .site-header.is-scrolled below, and the sentinel observer that
   sets it in js/site.js, dead code on the home page.

   .skip-link is `position: absolute` parked at top: -100px. Flipped to
   relative it became an in-flow inline box at the top of <body> and drew a
   real line box: a ~29px blank strip above the header on every load, with
   its :focus `top: 1rem` reinterpreted as a relative offset from that.

   The mobile .site-nav panel is absolutely positioned inside the header. The
   header's new z-index:1 trapped it in a stacking context that #main matched
   exactly, and #main is later in tree order, so an opened hamburger menu was
   painted over by the hero headline.

   `.mobile-bar` matched nothing at all; the element is .mobile-cta, and it is
   `position: fixed; z-index: 50` on its own account.

   None of it was needed. .page-ground is fixed, pointer-events: none and
   z-index: -1, so it is already behind every in-flow box without anything
   being lifted over it — see rule 1 in the block header above. */

/* The fills come off so the ground runs unbroken from header to footer.
   A ground that is visible in some sections and hidden in others reads
   as a rendering bug, not as a ground. The hairline borders these rules
   already carried do the section rhythm instead.
   index.html only — web.html keeps its bands, because it is Exhibit 4
   of a state filing and it gets printed to PDF. */
.has-ground .band { background: transparent; }
.has-ground .section-alt { background: transparent; }

@media (prefers-color-scheme: dark) {
  /* Light strokes on a dark ground read louder at the same alpha. */
  .page-ground-svg { opacity: 0.062; }
}

@media (prefers-reduced-motion: reduce) {
  .page-ground { animation: none; }
}

/* Print: the ground is decoration and it wastes toner. */
@media print {
  .page-ground { display: none; }
}

/* The hero's second column on web.html, where the nine-town map used to
   sit. Text now, not a drawing -- the valley schematic lives once, on
   index.html. */
.hero-aside { align-self: center; }
.hero-aside h2 { font-size: 1.35rem; margin-top: 0.35rem; }
.hero-aside p { margin-top: 0.75rem; color: var(--color-text-muted); }
.service-area-list {
  font-family: var(--font-mono);
  font-size: 0.95rem;
  line-height: 1.9;
  color: var(--color-text) !important;
  letter-spacing: 0.01em;
}

/* ---- legal.html: "The plan, in full" ------------------------------
   This section is dense by necessity — it is the whole offer in writing.
   What it must not be is undifferentiated. The failure mode it had, and
   the one to watch for if anyone edits it: every paragraph opening with
   a bold lead-in, so bold stops meaning anything and there is no way to
   scan. Bold is for the term that binds; the structure below does the
   scanning. */
.legal-lede {
  font-size: 1.1rem;
  color: var(--color-text-muted);
  margin-bottom: 1.25rem;
}
.terms-note {
  border-left: 3px solid var(--color-accent);
  background: var(--color-bg-raised);
  padding: 0.9rem 1.1rem;
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  margin-bottom: 1.5rem;
}
.terms-note p { margin-bottom: 0.65rem; }
.terms-note p:last-child { margin-bottom: 0; }

/* The two domain paths are a choice the reader has to make, so they are
   shown side by side and compared, not stacked as prose. */
.terms-paths { display: grid; gap: 1rem; margin: 1.1rem 0 1.25rem; }
@media (min-width: 820px) {
  .terms-paths { grid-template-columns: 1fr 1fr; align-items: start; }
}
.terms-path {
  border: 1px solid var(--color-border-strong);
  border-top-width: 3px;
  border-radius: var(--radius-sm);
  padding: 1rem 1.1rem 1.15rem;
  background: var(--color-bg-raised);
}
.terms-path h4 {
  font-size: 1rem;
  margin: 0 0 0.55rem;
}
.terms-path p, .terms-path li { max-width: none; font-size: 0.97rem; }
.terms-path ul { margin: 0.6rem 0 0; padding-left: 1.1rem; }
.terms-path li { margin-bottom: 0.4rem; }

.terms-caveat-head {
  font-size: 1rem;
  margin: 1.6rem 0 0.5rem;
  color: var(--color-text);
}
.terms-summary {
  border-left: 3px solid var(--color-action-border);
  padding: 0.75rem 1rem;
  background: var(--color-bg-sunken);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  margin-top: 1.1rem;
}

/* ===================================================================
   MOTION AND INTERACTION PASS — 2026-08-23
   ===================================================================
   Everything here is compositor-only (transform / opacity / colour) or
   a contained size interpolation. No blur, no glow, no shadow, no
   library, no external request. One easing curve does almost all of it:

       --ease-out: cubic-bezier(0.16, 1, 0.3, 1)     arriving
       --ease-std: cubic-bezier(0.4, 0, 0.2, 1)      changing state

   WHAT IS DELIBERATELY ABSENT, so nobody adds it back later:
   - No scroll-triggered fade-up reveals on content sections. Beyond
     being the strongest "template" tell there is, this site links
     directly to in-page anchors from its own footer and copy
     (web.html#pricing, legal.html#terms, individual FAQ items). A
     visitor landing mid-page on a slow phone would meet content that
     had not triggered yet.
   - Nothing on .price-plate. The price is a plain fact on a document
     filed with the state, not a thing to sell with motion.
   - Nothing eases the focus ring. A focus indicator must appear the
     instant focus lands or it disorients keyboard and switch users.
   - No spinner on the contact form. There is no server behind it —
     site.js opens a mailto: draft — and animating a request that is
     not happening is a small lie on a site that sells not lying.
   ------------------------------------------------------------------- */
:root {
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-std: cubic-bezier(0.4, 0, 0.2, 1);
  /* Lets height transition to `auto`, which is what the accordion needs.
     Ignored where unsupported, and the accordion falls back to snapping
     open exactly as it does today. */
  interpolate-size: allow-keywords;
}

/* ---- Page transitions ---------------------------------------------
   One line, and it is the highest-value motion on the site. The CSP has
   no connect-src, so every navigation here is a real document load —
   which is precisely the case this API exists for. Unsupported browsers
   navigate normally, so it cannot break anything. */
@view-transition { navigation: auto; }
::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 220ms;
  animation-timing-function: var(--ease-std);
}

/* ---- The disclosure accordions ------------------------------------
   web.html has ten of these: four price footnotes and six FAQ answers.
   The chevron already rotated; the answer itself snapped, which was the
   most visible unpolished moment on the site.

   ::details-content is the only way to do this without wrapping every
   answer in a div, and it is behind @supports so browsers without it
   simply keep today's instant open. site.js sets `open` directly on
   beforeprint, so the printed exhibit captures the final state and
   never a mid-transition frame. */
@supports selector(::details-content) {
  .faq-item::details-content {
    block-size: 0;
    overflow: hidden;
    transition: block-size 260ms var(--ease-std),
                content-visibility 260ms allow-discrete;
  }
  .faq-item[open]::details-content { block-size: auto; }
}
.faq-item .chev { transition: transform 220ms var(--ease-std); }

/* ---- Mobile menu icon ---------------------------------------------
   site.js already toggles aria-expanded correctly on all four pages;
   the three lines just never moved. A menu button that does not reflect
   its own state reads as unfinished, on the exact device this buyer
   uses. No markup change — nth-child against the existing <line>s.
   transform-box: view-box makes `center` resolve to the middle of the
   24x24 viewBox rather than a zero-height line's own bounding box. */
.nav-toggle svg line {
  transform-box: view-box;
  transform-origin: center;
  transition: transform 240ms var(--ease-out), opacity 140ms linear;
}
.nav-toggle[aria-expanded="true"] svg line:nth-child(1) {
  transform: rotate(45deg) translateY(6px);
}
.nav-toggle[aria-expanded="true"] svg line:nth-child(2) { opacity: 0; }
.nav-toggle[aria-expanded="true"] svg line:nth-child(3) {
  transform: rotate(-45deg) translateY(-6px);
}

/* ---- Hover and focus feedback that was missing ---------------------
   Before this, four selectors in the whole stylesheet had a :hover, and
   two of them snapped with no transition. .card had none at all despite
   containing a link. */
.btn-primary, .btn-secondary {
  transition: background-color 150ms var(--ease-std),
              border-color 150ms var(--ease-std),
              color 150ms var(--ease-std),
              transform 90ms var(--ease-std);
}
.btn-primary:active, .btn-secondary:active { transform: translateY(1px); }
.footer-grid a, .legal-toc a { transition: color 150ms var(--ease-std); }

/* Border only. No lift, no shadow — the site signals depth with edges. */
.card { transition: border-color 180ms var(--ease-std); }
@media (hover: hover) {
  .card:hover { border-color: var(--color-accent); }
}

/* The editorial underline. It is DRAWN AT REST and only its weight
   changes on hover -- it used to sit at scaleX(0) and reveal on hover,
   and that had to change.

   There is no hover on a phone. A reveal-on-hover underline therefore
   never appears there at all, which left colour as the only thing
   separating an in-text link from the sentence around it:

       light   #2c4c60 on #14161a body text = 1.99:1
       dark    #9fb6c9 on #eef1f3 body text = 1.85:1

   WCAG 1.4.1 wants 3:1 when colour is the only cue, so both schemes
   failed, on the device this buyer actually reads on, on a page whose
   stylesheet documents its ratios line by line. Note this was never a
   touch-only problem: a mouse user could not identify a link without
   hovering it either.

   opacity is compositor-only exactly like the old transform was, so
   this costs the same nothing on a phone. What is lost is the
   draw-from-the-left flourish; the underline can no longer animate its
   width because its width is now the resting state. */
.prose a, .card a, .quad-note a, .hero-aside a {
  position: relative;
  text-decoration: none;
}
.prose a::after, .card a::after, .quad-note a::after, .hero-aside a::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: -2px;
  height: 1px;
  background: currentColor;
  transform: scaleX(1);
  transform-origin: left center;
  /* 0.7, calculated not eyeballed. The resting underline IS the link
     affordance, so it is a non-text control cue and wants 3:1 against
     the ground it sits on. Accent composited over the page:

         light   70% -> #667d8b  3.71:1   (60% is 2.96 and misses)
         dark    70% -> #758694  4.82:1

     Hover takes it to 100% -- 7.82:1 light, 8.63:1 dark -- so there is
     still an obvious step up rather than a flat, always-loud rule. */
  opacity: 0.7;
  transition: opacity 240ms var(--ease-out);
}
/* Focus is not guarded: a keyboard ring must not depend on a pointer. */
.prose a:focus-visible::after,
.card a:focus-visible::after,
.quad-note a:focus-visible::after,
.hero-aside a:focus-visible::after {
  opacity: 1;
}
@media (hover: hover) {
  .prose a:hover::after,
  .card a:hover::after,
  .quad-note a:hover::after,
  .hero-aside a:hover::after {
    opacity: 1;
  }
}

/* Form fields warm up on focus. The focus RING itself stays instant —
   see the note at the top of this block. */
.field input, .field select, .field textarea {
  transition: border-color 130ms var(--ease-std);
}
.field input:focus, .field select:focus, .field textarea:focus {
  border-color: var(--color-accent);
}

/* ---- Header responds to scroll -------------------------------------
   A firmer hairline, not a shadow. The stylesheet already argues that a
   soft ambient shadow is the strongest SaaS-marketing-page tell, and
   this site draws depth with edges everywhere else. */
.site-header {
  transition: border-bottom-color 200ms var(--ease-std);
}
.header-inner { transition: padding-block 200ms var(--ease-std); }
.site-header.is-scrolled { border-bottom-color: var(--color-accent); }
.site-header.is-scrolled .header-inner { padding-block: 0.38rem; }

@media (prefers-reduced-motion: reduce) {
  .faq-item::details-content,
  .faq-item .chev,
  .nav-toggle svg line,
  .btn-primary, .btn-secondary,
  .footer-grid a, .legal-toc a,
  .card,
  .prose a::after, .card a::after, .quad-note a::after, .hero-aside a::after,
  .field input, .field select, .field textarea,
  .site-header, .header-inner { transition: none; }
  ::view-transition-group(*),
  ::view-transition-old(root),
  ::view-transition-new(root) { animation: none !important; }
}

/* ===================================================================
   THE HERO PLOTS ITSELF — index.html
   ===================================================================
   The survey draws in on load: the valley edge first, the river behind
   it, then the nine towns east to west -- Boise first, Caldwell last --
   following the water downstream, then the collar. About 2.4 seconds end to end, and then it is done
   and quiet forever. Reveal-then-settle, not perpetual motion.

   PURE CSS. No JavaScript touches this at all, which is what makes the
   three hard cases fall out for free:
     - JS off: CSS animations still run. The hero plots itself anyway.
     - prefers-reduced-motion: the whole block is inside a
       (no-preference) query, so a reduced-motion visitor gets the
       finished drawing immediately, at full opacity. Not a blank box —
       the settled final frame, which is the composition either way.
     - LCP: nothing here is the largest paint element. The headline is,
       it is in the other column, and it is never opacity-gated.

   The dash trick works because tools/make-ridge.py emits pathLength="1"
   on the two paths, normalising their length. dasharray:1 / dashoffset:1
   is therefore exactly "fully hidden" regardless of the real geometry,
   and nobody has to measure anything or maintain a magic number.
   ------------------------------------------------------------------- */
@media (prefers-reduced-motion: no-preference) {
  .quad-edge, .quad-river, .quad-river-wide {
    stroke-dasharray: 1;
    stroke-dashoffset: 1;
    animation: quad-draw 1.5s var(--ease-out) forwards;
  }
  /* The river follows the outline in, rather than racing it. */
  .quad-river-wide { animation-delay: 0.32s; animation-duration: 1.3s; }
  .quad-river      { animation-delay: 0.38s; animation-duration: 1.3s; }

  .quad-towns g {
    opacity: 0;
    animation: quad-town-in 0.5s var(--ease-out) forwards;
    /* --i is written by the generator, sorted by descending longitude, so
       the towns arrive east to west behind the water, in the direction the
       river actually runs. */
    animation-delay: calc(0.75s + var(--i) * 0.07s);
  }
  .quad-towns .quad-dot {
    transform-box: fill-box;
    transform-origin: center;
  }

  .quad-collar {
    opacity: 0;
    animation: quad-fade 0.6s var(--ease-out) 1.55s forwards;
  }
  .quad-note {
    opacity: 0;
    animation: quad-fade 0.6s var(--ease-out) 1.75s forwards;
  }
}

@keyframes quad-draw   { to { stroke-dashoffset: 0; } }
@keyframes quad-fade   { to { opacity: 1; } }
@keyframes quad-town-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* ---- Repeat visits skip the plot-in --------------------------------
   `html.returning` is set by an inline <head> script on index.html from
   a localStorage flag. Specificity 0,0,2,1 against the animation rules'
   0,0,1,1, so it wins wherever it sits in the file and needs no
   !important.

   Orthogonal to reduced-motion: that visitor already gets the settled
   frame. And with JS off the flag is never set, so the animation plays
   every time, which is exactly today's behaviour. Nothing regresses in
   either direction. */
html.returning .quad-edge,
html.returning .quad-river,
html.returning .quad-river-wide,
html.returning .quad-towns g,
html.returning .quad-collar,
html.returning .quad-note {
  animation: none;
  opacity: 1;
  stroke-dashoffset: 0;
}

/* ---- The brand responds to hover ----------------------------------
   ATTEMPTED AND REVERTED, 2026-08-23: drawing an amber line down the
   centre of the river slot in the mark, matching the hero's draw-in.
   It renders, and at the size the mark is actually used it is invisible
   -- the header logo is 27px tall, the river slot is ~11% of that, and
   the stroke lands at roughly one pixel. Do not retry it at this scale.
   (It would work on a mark rendered at 80px or more, if one ever exists.)

   What replaces it is the thing the brand link was missing anyway: any
   hover feedback at all. `color` inherits, including into the <use>
   shadow tree, so the mark follows the text without needing to be
   targeted separately -- which is also why the descendant-selector
   approach failed above. Accent, not amber: amber stays reserved for
   the CTA and for water. */
.brand { transition: color 160ms var(--ease-std); }
.brand:focus-visible { color: var(--color-accent); }
@media (hover: hover) {
  .brand:hover { color: var(--color-accent); }
}

@media (prefers-reduced-motion: reduce) {
  .brand { transition: none; }
}

/* ---- Agency inbound-SEO pages (pricing, how-it-works, faq, industry and
   city pages) — tools/agency/build_pages.py, added 2026-08-26 ------------
   Two small additions, both built from existing tokens so the new pages
   read as the same site rather than a bolted-on section. */

/* Breadcrumb trail on industry/city pages. Muted list text, current page
   not a link, everything else clears 3:1 as body copy already does. */
.crumbs {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  font-size: 0.85rem;
  color: var(--color-text-muted);
  margin-bottom: 1rem;
}
.crumbs a { color: inherit; }
.crumbs .crumb-sep { opacity: 0.6; }
.crumbs .crumb-current { color: var(--color-text); font-weight: 600; }

/* A .card that is itself the link, used for the "websites by industry" and
   "websites by city" grids on pricing.html. .card already supplies the
   border, radius, padding and hover state; this only fixes the anchor's
   own box and text behaviour. */
.card-link { display: block; text-decoration: none; color: inherit; }
.card-link h3 { color: var(--color-accent); }
