/* GENERATED by build.py from styles/*.css — DO NOT EDIT THIS FILE. */

/* ── styles/00-tokens.css ── */
/* ============================================================================
   KIRKPATRICK SALES & RENTALS — design tokens
   Neutral industrial base · per-pillar accent override

   Read this before adding CSS anywhere else:
   - Never hard-code a hex. Use a var. The old site hard-coded #F13200 into three
     gradients, which is why rebranding it meant find-and-replace.
   - Never hard-code a font-size on a structural heading or body copy. Use the
     type vars so one change ripples sitewide.
   - --body is a SIZE. Font families are --font-display / --font-body. Do not
     overload --body as a family; that collision had to be worked around on
     another site.
   ========================================================================== */

:root{
  /* ---- surfaces: charcoal / near-black base ---- */
  --bg:#101012;
  --bg-2:#17171a;
  --surface:#1d1d21;
  --surface-2:#24242a;
  --line:#2b2b31;
  --line-strong:#3a3a42;
  --text:#f4f4f2;
  --muted:#a6a6ad;
  --heading-color:#ffffff;
  --footer-bg:#0a0a0c;

  /* ---- light sections (used sparingly, for contrast breaks) ---- */
  --bg-light:#f4f4f2;
  --on-light:#101012;
  --muted-light:#5c5c63;
  --line-light:#d9d9d4;

  --hero-overlay:linear-gradient(180deg,rgba(16,16,18,.20) 0%,rgba(16,16,18,.86) 78%);

  /* ---- accent: NEUTRAL default, overridden per pillar below ----
     Every component reads var(--accent). That is what makes re-theming total
     and free — no component ever needs a per-pillar variant. */
  --accent:#c9ccd1;
  --accent-bright:#e6e8ea;
  --accent-deep:#4a4d52;    /* accent-colored TEXT on a light background */
  --accent-ink:#101012;     /* text that sits ON an accent fill */
  --accent-text:#c9ccd1;    /* accent-colored SMALL TEXT on the dark base */
  --accent-on-ink:#c9ccd1;  /* accent-colored text on an --accent-ink fill */
  --accent-hover:#e6e8ea;   /* .btn-accent fill on hover/focus */

  /* ---- type: families ---- */
  --font-display:'Oswald','Arial Narrow',Impact,sans-serif;
  --font-body:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;

  /* ---- type: sizes ---- */
  --heading:clamp(2rem,4.4vw,3.5rem);
  --heading-2:clamp(1.5rem,2.8vw,2.25rem);
  --heading-3:clamp(1.15rem,1.8vw,1.4rem);
  --body:1rem;
  --body-sm:0.875rem;
  --body-xs:0.75rem;
  --eyebrow-size:0.72rem;
  --leading:1.65;
  --tracking-display:0.01em;
  --tracking-eyebrow:0.18em;

  /* ---- layout ---- */
  --wrap:1200px;
  --wrap-narrow:820px;
  --nav-h:64px;
  --gap:clamp(1rem,2.4vw,1.75rem);
  --section-y:clamp(3rem,7vw,6rem);

  /* ---- form language ----
     BOSS forces border-radius:0 across their entire component set. Square
     corners are the strongest transferable signal from their brand, and they
     suit heavy equipment generally. Keep radii minimal. */
  --btn-radius:2px;
  --card-radius:3px;
  --border:1px solid var(--line);
  --shadow:0 12px 34px rgba(0,0,0,.45);
  --shadow-sm:0 4px 14px rgba(0,0,0,.30);
  --focus-ring:0 0 0 3px var(--accent-bright);

  /* ---- motion (FLW Motion Standard values) ---- */
  --t-fast:.15s ease;
  --t:.25s ease;
  --t-slow:1.1s ease;
}

/* ---- PER-PILLAR ACCENT OVERRIDE -------------------------------------------
   Applied to <body class="theme-X"> for pillar pages, driven by page.pillar in
   the registry — so a page physically cannot ship the wrong accent.

   The BARE class (no `body`) also works on any <section>, which is how the
   neutral homepage shows three pillar teasers each in its own correct color.
   Custom properties cascade, so no extra rules are needed.

   --accent-deep exists because #FFB81C on white fails contrast; #7A5400 passes.

   --accent-text and --accent-on-ink exist for the same reason, pointed the
   other way, and only BOSS actually needs them to differ from --accent:

     --accent-text     #EE312A on --surface is 4.09:1 and on --bg-2 is 4.35:1.
                       Both are AA fails for anything under 24px, which is every
                       eyebrow, badge and inline link on the site. #FF4A43 —
                       already BOSS's own --accent-bright — measures 5.05:1 to
                       5.71:1 across the four dark surfaces. Bobcat orange
                       (5.81:1) and safety yellow (9.70:1) never needed it, so
                       for them the two values are simply equal.

     --accent-on-ink   .cta-band .btn-accent INVERTS the pair: the fill becomes
                       --accent-ink and the text becomes the accent. For three
                       themes --accent-ink is near-black and the accent reads at
                       6.6:1 to 11.8:1. For BOSS --accent-ink is WHITE, so the
                       same rule produces red-on-white at 4.11:1 — a fail. There
                       it takes --accent-deep's value instead, at 9.34:1.

   These must be re-declared in FULL in every theme block below. A `var(--accent)`
   fallback would not work: a custom property is substituted at computed-value
   time on the element that DECLARES it, so `--accent-text:var(--accent)` on
   :root freezes to the neutral grey and never sees the pillar override.
   -------------------------------------------------------------------------- */

/* Bobcat orange */
body.theme-equipment, .theme-equipment{
  --accent:#F47216;
  --accent-bright:#FF8A33;
  --accent-deep:#8A3D08;
  --accent-ink:#101012;
  --accent-text:#F47216;    /* 5.81:1 on --surface — no adjustment needed */
  --accent-on-ink:#F47216;  /* 6.58:1 on the near-black ink fill */
  --accent-hover:#FF8A33;   /* ink is near-black, so a lighter hover RAISES contrast */
}

/* BOSS red — #EE312A read from BOSS's live production CSS (96 occurrences,
   sets their masthead and every hover state). Brandfetch's #EE312B is a
   scraper's value, not BOSS's. */
body.theme-boss, .theme-boss{
  --accent:#EE312A;
  --accent-bright:#FF4A43;
  --accent-deep:#8E1216;
  --accent-ink:#FFFFFF;
  --accent-text:#FF4A43;    /* #EE312A is 4.09:1 as small text on --surface */
  --accent-on-ink:#8E1216;  /* --accent-ink is WHITE here; red on white is 4.11:1 */
  --accent-hover:#C4231D;   /* see the DECISION note below — hover DARKENS here */
}

/* Safety yellow — the color of the rental fleet itself */
body.theme-rentals, .theme-rentals{
  --accent:#FFB81C;
  --accent-bright:#FFC94D;
  --accent-deep:#7A5400;
  --accent-ink:#101012;
  --accent-text:#FFB81C;    /* 9.70:1 on --surface — no adjustment needed */
  --accent-on-ink:#FFB81C;  /* 10.97:1 on the near-black ink fill */
  --accent-hover:#FFC94D;   /* ink is near-black, so a lighter hover RAISES contrast */
}


/* ============================================================================
   DECISION (2026-07-30, owner) — BOSS red stays brand-exact, AA fail accepted

   White on BOSS red #EE312A measures 4.11:1. WCAG AA wants 4.5 for body-size
   text, so every .btn-accent and the CTA band on the 11 BOSS pages is a
   knowing, documented failure. This is BOSS's own pairing, used on their
   production site, and the owner chose brand fidelity over the ratio.

   This is a decision, not an oversight. Do not "fix" it in a later audit
   without asking. The two alternatives were measured:
       --accent-ink: #101012   black on red        4.62:1  (passes, off-brand)
       --accent:     #E91A13   same hue, darker    4.54:1  (passes, near-identical)

   Consequence: BOSS pages score 96 on Lighthouse accessibility instead of 100,
   and the ONLY failing audit is color-contrast. Everything else is 100.

   WHAT WAS FIXED: the HOVER state, which is not brand-constrained. It used
   --accent-bright #FF4A43, dropping white to 3.33:1 — hover was HARDER to read
   than rest, which is backwards. BOSS now darkens on hover to #C4231D (5.82:1),
   which passes. The other two themes have a near-black ink, so for them a
   lighter hover raises contrast and they keep brightening.
   ========================================================================== */

/* ── styles/10-reset.css ── */
/* ============================================================================
   Reset & base
   ========================================================================== */

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

html{
  -webkit-text-size-adjust:100%;
  scroll-behavior:smooth;
  scroll-padding-top:calc(var(--nav-h) + 1rem);
}

body{
  min-height:100vh;
  background:var(--bg);
  color:var(--text);
  font-family:var(--font-body);
  font-size:var(--body);
  line-height:var(--leading);
  -webkit-font-smoothing:antialiased;
  text-rendering:optimizeLegibility;
}

img,picture,svg,video,canvas{ display:block; max-width:100%; height:auto; }

input,button,textarea,select{ font:inherit; color:inherit; }

h1,h2,h3,h4,h5,h6{
  font-family:var(--font-display);
  color:var(--heading-color);
  font-weight:600;
  line-height:1.1;
  letter-spacing:var(--tracking-display);
  text-wrap:balance;
}
h1{ font-size:var(--heading); }
h2{ font-size:var(--heading-2); }
h3{ font-size:var(--heading-3); }

p,li{ text-wrap:pretty; }

a{ color:inherit; text-decoration:none; }
a:hover{ color:var(--accent-text); }

ul,ol{ list-style:none; padding:0; }

table{ border-collapse:collapse; width:100%; }

/* Visible focus everywhere. Never remove this without replacing it. */
:focus-visible{
  outline:none;
  box-shadow:var(--focus-ring);
  border-radius:var(--btn-radius);
}

.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; top:-100%; left:0; z-index:2000;
  padding:.75rem 1.25rem;
  background:var(--accent); color:var(--accent-ink);
  font-weight:600;
}
.skip-link:focus{ top:0; }

/* ── styles/20-layout.css ── */
/* ============================================================================
   Layout — wrapper, header, nav, footer, sections

   BREAKPOINTS (the only two in this codebase — do not add a third):
     @media (min-width:760px)    tablet / 2-col
     @media (min-width:1024px)   desktop / full nav / 3-4 col
   ========================================================================== */

.wrap{
  width:100%;
  max-width:var(--wrap);
  margin-inline:auto;
  padding-inline:clamp(1rem,4vw,2rem);
}
.wrap-narrow{ max-width:var(--wrap-narrow); }

section{ padding-block:var(--section-y); }
section.tight{ padding-block:clamp(2rem,4vw,3rem); }

/* ---------------------------------------------------------------- header -- */

.site-header{
  position:sticky; top:0; z-index:1000;
  background:rgba(16,16,18,.94);
  backdrop-filter:saturate(140%) blur(10px);
  border-bottom:1px solid var(--line);
}

.header-inner{
  display:flex; align-items:center; gap:1rem;
  min-height:var(--nav-h);
  flex-wrap:wrap;
}

.brand{ flex:0 0 auto; margin-right:auto; }
.brand img{ width:auto; height:42px; }

.header-actions{ margin-left:auto; }
.btn-call-label{ display:none; }

/* CSS-only mobile menu: the checkbox is the state, no JS required to open. */
.nav-toggle{ display:none; }

.hamburger{
  order:3;
  display:flex; flex-direction:column; justify-content:center; gap:5px;
  width:44px; height:44px; padding:10px;
  cursor:pointer;
}
.hamburger span{
  display:block; height:2px; width:100%;
  background:var(--text);
  transition:transform var(--t),opacity var(--t);
}
.nav-toggle:focus-visible + .hamburger{ box-shadow:var(--focus-ring); }
.nav-toggle:checked + .hamburger span:nth-child(1){ transform:translateY(7px) rotate(45deg); }
.nav-toggle:checked + .hamburger span:nth-child(2){ opacity:0; }
.nav-toggle:checked + .hamburger span:nth-child(3){ transform:translateY(-7px) rotate(-45deg); }

.site-nav{
  order:4;
  flex-basis:100%;
  display:none;
  padding-block:.5rem 1rem;
}
.nav-toggle:checked ~ .site-nav{ display:block; }

.site-nav ul{ display:flex; flex-direction:column; gap:.15rem; }
.site-nav a{
  display:block;
  padding:.7rem .25rem;
  font-family:var(--font-display);
  font-size:1.05rem;
  font-weight:500;
  text-transform:uppercase;
  letter-spacing:.04em;
  border-bottom:1px solid var(--line);
}
.site-nav a.is-current{ color:var(--accent-text); }
.site-nav .ext{ font-size:.8em; opacity:.6; }

@media (min-width:1024px){
  .hamburger{ display:none; }
  .site-nav{
    display:block; order:2;
    flex-basis:auto; padding-block:0;
    margin-inline:auto;
  }
  .site-nav ul{ flex-direction:row; gap:.25rem; align-items:center; }
  .site-nav a{
    padding:.5rem .7rem;
    font-size:.82rem;
    border-bottom:2px solid transparent;
    white-space:nowrap;
  }
  .site-nav a:hover{ border-bottom-color:var(--line-strong); }
  .site-nav a.is-current{ border-bottom-color:var(--accent); }
  .brand img{ height:48px; }
  .btn-call-label{ display:inline; }
}

/* ---------------------------------------------------------------- footer -- */

.site-footer{
  background:var(--footer-bg);
  border-top:3px solid var(--accent);
  padding-top:var(--section-y);
  margin-top:0;
}

.footer-grid{
  display:grid; gap:var(--gap);
  grid-template-columns:1fr;
}
@media (min-width:760px){ .footer-grid{ grid-template-columns:repeat(2,1fr); } }
@media (min-width:1024px){ .footer-grid{ grid-template-columns:1.2fr 1fr 1fr 1.4fr; } }

.footer-brand img{ height:56px; width:auto; margin-bottom:.75rem; }
.footer-tagline{
  color:var(--muted);
  font-family:var(--font-display);
  text-transform:uppercase;
  letter-spacing:.06em;
  font-size:var(--body-sm);
}

.footer-head{
  font-size:var(--eyebrow-size);
  text-transform:uppercase;
  letter-spacing:var(--tracking-eyebrow);
  color:var(--accent);
  margin-bottom:1rem;
}

.footer-links li a{
  display:block; padding:.3rem 0;
  color:var(--muted); font-size:var(--body-sm);
}
.footer-links li a:hover{ color:var(--text); }

.social-link{
  display:inline-flex; align-items:center; justify-content:center;
  width:40px; height:40px; margin-top:1rem;
  border:var(--border); color:var(--muted);
}
.social-link:hover{ color:var(--accent-ink); background:var(--accent); border-color:var(--accent); }

.footer-bottom{
  margin-top:var(--section-y);
  padding-block:1.5rem;
  border-top:1px solid var(--line);
  color:var(--muted); font-size:var(--body-xs);
}
.footer-fineprint{ margin-top:.35rem; opacity:.75; }

/* ------------------------------------------------------------- NAP block -- */

.nap{ font-style:normal; margin-bottom:1rem; }
.nap p{ margin-bottom:.4rem; font-size:var(--body-sm); color:var(--muted); }
.nap-phone a,.nap-email a{ color:var(--text); font-weight:600; }
.nap-phone a:hover,.nap-email a:hover{ color:var(--accent-text); }
.nap-email a{ font-weight:400; word-break:break-word; }

.hours{ font-size:var(--body-sm); max-width:230px; }
.hours th{
  text-align:left; font-weight:600; color:var(--text);
  padding:.18rem .75rem .18rem 0; width:3.2em;
  font-family:var(--font-body);
}
.hours td{ padding:.18rem 0; color:var(--muted); }
/* Closed days are de-emphasised, not made unreadable. At opacity .6 the word
   "Closed" composited to rgb(104,104,109) on the footer — 3.57:1, an AA fail
   on the one piece of information a visitor most often comes to a contractor's
   site for. .8 keeps the visual hierarchy and measures 5.54:1 in the footer,
   4.96:1 in the /contact panel. Do not push it back down. */
.hours tr.is-closed td{ opacity:.8; }

/* ------------------------------------------------------------------ map -- */

.map-embed{ border:var(--border); line-height:0; }
.map-embed iframe{ width:100%; }
.map-directions{ margin-top:.75rem; }

/* ------------------------------------------------------------- CTA band -- */

.cta-band{
  background:var(--accent);
  color:var(--accent-ink);
  padding-block:clamp(2.5rem,5vw,4rem);
}
.cta-inner{
  display:flex; flex-direction:column; gap:1.5rem;
  align-items:flex-start;
}
.cta-band h2{ color:var(--accent-ink); }
/* No opacity here. This paragraph sits on a saturated accent fill, where 15%
   of transparency costs more contrast than it does on a neutral surface: on
   BOSS red it took white body copy from 4.11:1 to 3.30:1, and on Bobcat orange
   5.42:1 to 6.58:1 was given away for nothing. The hierarchy is already
   carried by the h2 above it. */
.cta-band p{ margin-top:.5rem; max-width:52ch; }
.cta-actions{ display:flex; flex-wrap:wrap; gap:.75rem; }

/* On an accent fill the buttons must invert — the accent is the background. */
/* Inverted: the fill is --accent-ink and the text is the accent. On BOSS
   --accent-ink is WHITE, so var(--accent) here is red-on-white at 4.11:1.
   --accent-on-ink is that same pairing done safely (9.34:1) and is simply
   equal to --accent on the three themes with a near-black ink. */
.cta-band .btn-accent{ background:var(--accent-ink); color:var(--accent-on-ink); }
/* The hover flips the fill to pure black, which un-does the reason
   --accent-on-ink exists: BOSS's deep red #8E1216 is picked to sit on WHITE and
   drops to ~2:1 on black. On a black fill the full-strength accent is the right
   one and is 4.8:1 to 12.6:1 across the four themes. */
.cta-band .btn-accent:hover{ background:#000; color:var(--accent); }
.cta-band .btn-ghost{ border-color:var(--accent-ink); color:var(--accent-ink); }
.cta-band .btn-ghost:hover{ background:var(--accent-ink); color:var(--accent-on-ink); }

@media (min-width:760px){
  .cta-inner{ flex-direction:row; align-items:center; justify-content:space-between; }
  .cta-actions{ flex-shrink:0; }
}

/* ── styles/30-components.css ── */
/* ============================================================================
   Components — buttons, eyebrow, section heads, cards, grids, tables,
                badges, placeholder tiles, breadcrumbs

   BREAKPOINTS: 760px and 1024px. There is no third one. Do not add one.

   COLOUR: every rule below reads var(--accent). Nothing here knows what
   pillar it is on — the theme-* class on <body> (or on a pillar_card wrapper)
   re-points the variable and the whole component re-themes for free. If you
   ever find yourself writing `.theme-boss .card { … }`, stop: the token was
   the wrong shape, not the component.

   RADII: --btn-radius / --card-radius are near-square on purpose. BOSS forces
   border-radius:0 across their entire component set and it suits heavy
   equipment. Do not round these up.
   ========================================================================== */


/* ------------------------------------------------------------- buttons -- */
/* .btn already ships in partials/header.html, cta-band.html and map.html —
   those class names are the contract, not a suggestion. Note the reset sets
   a:hover{color:var(--accent-text)} at specificity (0,1,1), so every variant needs
   its OWN :hover colour at (0,2,0) or the accent leaks through on hover. */

.btn{
  display:inline-flex; align-items:center; justify-content:center; gap:.5rem;
  max-width:100%;
  padding:.8rem 1.4rem;
  font-family:var(--font-display);
  font-size:.9rem; font-weight:600; line-height:1.1;
  text-transform:uppercase; letter-spacing:.06em; text-align:center;
  border:2px solid transparent;
  border-radius:var(--btn-radius);
  cursor:pointer;
  transition:background var(--t),color var(--t),border-color var(--t);
}

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

.btn-ghost{ background:transparent; color:var(--text); border-color:var(--line-strong); }
.btn-ghost:hover,
.btn-ghost:focus-visible{ color:var(--accent-text); border-color:var(--accent); }

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

.btn-sm{ padding:.5rem .9rem; font-size:.78rem; }
.btn-lg{ padding:1rem 1.9rem; font-size:1rem; }

/* .7 put this glyph at 4.12:1 against the accent fill it sits on. It is
   aria-hidden so it is decorative to AT, but it is still the only visual
   signal that a link leaves the site. .85 measures 5.42:1. */
.btn .ext{ font-size:.8em; opacity:.85; }

/* Mobile-first: stacked and full width, so a long label never overflows at
   375px. Side by side from the tablet breakpoint up. */
.btn-pair{ display:grid; gap:.75rem; margin-top:1.5rem; }
@media (min-width:760px){
  .btn-pair{ display:flex; flex-wrap:wrap; align-items:center; }
}

/* On a light section the ghost outline disappears — re-point it. */
.on-light .btn-ghost{ color:var(--on-light); border-color:var(--line-light); }
.on-light .btn-ghost:hover{ color:var(--accent-deep); border-color:var(--accent-deep); }


/* ------------------------------------------------------------- eyebrow -- */

.eyebrow{
  font-family:var(--font-display);
  font-size:var(--eyebrow-size); font-weight:600;
  text-transform:uppercase; letter-spacing:var(--tracking-eyebrow);
  color:var(--accent-text);
  margin-bottom:.6rem;
}
.on-light .eyebrow{ color:var(--accent-deep); }


/* -------------------------------------------------------- section head -- */

.section-head{ max-width:70ch; margin-bottom:clamp(1.5rem,3vw,2.5rem); }
.section-head--center{ margin-inline:auto; text-align:center; }
.section-head-body{ margin-top:.85rem; color:var(--muted); }
.on-light .section-head-body{ color:var(--muted-light); }


/* ---------------------------------------------------------------- grid -- */

.grid{ display:grid; gap:var(--gap); grid-template-columns:1fr; }
@media (min-width:760px){
  .grid--2,.grid--3,.grid--4{ grid-template-columns:repeat(2,minmax(0,1fr)); }
}
@media (min-width:1024px){
  .grid--3{ grid-template-columns:repeat(3,minmax(0,1fr)); }
  .grid--4{ grid-template-columns:repeat(4,minmax(0,1fr)); }
}


/* --------------------------------------------------------------- cards -- */

.card{
  display:flex; flex-direction:column;
  background:var(--surface);
  border:var(--border);
  border-radius:var(--card-radius);
  overflow:hidden;
  position:relative;
  transition:border-color var(--t),transform var(--t);
}
.card:hover{ border-color:var(--line-strong); }

.card-media{
  position:relative;
  aspect-ratio:4/3;
  background:var(--bg-2);
  overflow:hidden;
}
/* aspect-ratio on the wrapper is what holds the layout still. The data has no
   intrinsic width/height for any photo, so width/height attributes are not an
   option and this is the only thing standing between us and layout shift. */
.card-media img{ width:100%; height:100%; object-fit:cover; }
.card:hover .card-media img{ transform:scale(1.03); }
.card-media img{ transition:transform var(--t-slow); }

.card-badge{ position:absolute; top:.6rem; left:.6rem; z-index:1; }

.card-body{ display:flex; flex-direction:column; padding:1.15rem; flex:1 1 auto; }
.card-title{ font-size:var(--heading-3); }
.card-title a{ color:inherit; }
.card-title a:hover{ color:var(--accent-text); }
.card-blurb{ margin-top:.6rem; color:var(--muted); font-size:var(--body-sm); }
.card-meta{
  margin-top:.75rem;
  font-size:var(--body-xs); text-transform:uppercase; letter-spacing:.1em;
  color:var(--muted);
}
.card-variants{ margin-top:.35rem; font-size:var(--body-sm); color:var(--muted); }
.card-foot{ padding:0 1.15rem 1.15rem; margin-top:auto; }

/* Whole-card click target driven off the title link. The ::after overlay sits
   under .card-foot content so a nested button still wins. */
.card-title a.stretched::after{
  content:""; position:absolute; inset:0; z-index:0;
}
.card-foot{ position:relative; z-index:1; }


/* ---------------------------------------------------- placeholder tiles -- */
/* No <img> at all. build.py hard-fails on a src that points at a file which
   is not in site/images/, so an "artwork pending" record must render markup
   that has no src to be wrong about. Every BOSS model is in this state today. */

.ph{
  display:flex; align-items:center; justify-content:center;
  width:100%; height:100%; min-height:9rem;
  padding:1rem;
  text-align:center;
  background:
    repeating-linear-gradient(
      135deg,
      var(--bg-2) 0 10px,
      var(--surface-2) 10px 20px
    );
  border-bottom:1px solid var(--line);
}
.ph-label{
  font-family:var(--font-display);
  font-size:var(--body-sm); font-weight:600;
  text-transform:uppercase; letter-spacing:.08em;
  color:var(--muted);
}


/* --------------------------------------------------------------- specs -- */

.specs{ margin-top:.85rem; display:flex; flex-direction:column; gap:.3rem; }
.specs li{
  position:relative;
  padding-left:1.1rem;
  font-size:var(--body-sm);
  color:var(--muted);
}
.specs li::before{
  content:""; position:absolute; left:0; top:.62em;
  width:6px; height:2px;
  background:var(--accent);
}
.on-light .specs li{ color:var(--muted-light); }


/* --------------------------------------------------------------- price -- */
/* Rendered by ui.price(). When site.show_prices is false the ONLY thing that
   reaches the DOM is .price-call — there is no number to style. */

.price{ margin-top:1.1rem; }

.price-lead{ display:flex; flex-direction:column; gap:.1rem; }
.price-label{
  font-size:var(--body-xs); text-transform:uppercase; letter-spacing:.1em;
  color:var(--muted);
}
.price-value{
  font-family:var(--font-display);
  font-size:1.7rem; line-height:1.1;
  color:var(--accent-text);
}
.price-from{
  font-size:.55em; text-transform:uppercase; letter-spacing:.08em;
  color:var(--muted);
}
.price-period{ font-size:.5em; color:var(--muted); }
.price-terms{ font-size:var(--body-sm); color:var(--muted); }

.price-secondary{
  margin-top:.55rem;
  display:flex; flex-direction:column; gap:.15rem;
  font-size:var(--body-sm); color:var(--muted);
}

.price-call{
  font-family:var(--font-display);
  font-size:1.25rem; text-transform:uppercase; letter-spacing:.04em;
  color:var(--accent-text);
}

.price-disclaimer{ margin-top:.6rem; font-size:var(--body-xs); color:var(--muted); opacity:.8; }

.promo-list{ margin-top:.8rem; display:flex; flex-direction:column; gap:.25rem; }
.promo-list li{
  font-family:var(--font-display);
  font-size:var(--body-sm); text-transform:uppercase; letter-spacing:.05em;
  color:var(--accent-bright);
}

.on-light .price-value,
.on-light .price-call,
.on-light .promo-list li{ color:var(--accent-deep); }


/* -------------------------------------------------------------- badges -- */

.badge{
  display:inline-block;
  padding:.2rem .55rem;
  font-family:var(--font-display);
  font-size:var(--body-xs); font-weight:600; line-height:1.4;
  text-transform:uppercase; letter-spacing:.08em;
  color:var(--muted);
  border:1px solid var(--line-strong);
  border-radius:var(--btn-radius);
}
.badge-accent{ color:var(--accent-text); border-color:var(--accent); }
.badge-solid{ background:var(--accent); color:var(--accent-ink); border-color:var(--accent); }
.badge-row{ display:flex; flex-wrap:wrap; gap:.4rem; margin-top:.4rem; }


/* -------------------------------------------------------- pillar cards -- */
/* Each carries its own theme-* class, so --accent is already correct inside.
   The homepage <body> has no theme at all and stays neutral around them. */

.pillar-card{ border-top:3px solid var(--accent); }
.pillar-card .card-title{ font-size:var(--heading-3); text-transform:uppercase; }
.pillar-cue{
  font-family:var(--font-display);
  font-size:var(--body-sm); text-transform:uppercase; letter-spacing:.08em;
  color:var(--accent-text);
}
.pillar-card:hover .pillar-cue{ color:var(--accent-bright); }


/* --------------------------------------------------------- rate tables -- */
/* The rate table has 5 columns and cannot shrink to 375px without either
   lying about the numbers or wrapping into soup. So it scrolls sideways
   inside its own box and the machine-name column stays pinned.
   REQUIRED markup (rental_row() says the same thing):
     <div class="table-scroll" tabindex="0" role="region" aria-label="Rental rates">
       <table class="rate-table"> … </table>
     </div>
   tabindex="0" is what lets a keyboard-only user scroll it. */

.table-scroll{
  overflow-x:auto;
  -webkit-overflow-scrolling:touch;
  border:var(--border);
  border-radius:var(--card-radius);
  background:var(--surface);
}
.table-scroll:focus-visible{ box-shadow:var(--focus-ring); }

.rate-table{ min-width:36rem; font-size:var(--body-sm); }

.rate-table caption{
  padding:.9rem 1rem;
  text-align:left;
  font-size:var(--body-xs); text-transform:uppercase; letter-spacing:var(--tracking-eyebrow);
  color:var(--muted);
  border-bottom:1px solid var(--line);
}

.rate-table th,
.rate-table td{
  padding:.7rem .9rem;
  border-bottom:1px solid var(--line);
  text-align:left;
  vertical-align:top;
}
.rate-table tr:last-child th,
.rate-table tr:last-child td{ border-bottom:0; }

.rate-table thead th{
  background:var(--surface-2);
  font-family:var(--font-display);
  font-size:var(--body-xs); font-weight:600;
  text-transform:uppercase; letter-spacing:.08em;
  color:var(--text);
  white-space:nowrap;
  vertical-align:bottom;
}
.rate-table thead th span{
  display:block;
  font-family:var(--font-body);
  font-size:var(--body-xs); font-weight:400; letter-spacing:0; text-transform:none;
  color:var(--muted);
}

/* Numbers right-aligned; the name column stays left. */
.rate-table th:not(:first-child),
.rate-table td{ text-align:right; white-space:nowrap; }

/* Pinned first column — without it you scroll right and lose track of which
   machine you are reading. z-index 2 on the header cell so it stays above the
   pinned body cells while both are sticky. */
.rate-table thead th:first-child{ position:sticky; left:0; z-index:2; background:var(--surface-2); }
.rate-table tbody th:first-child{
  position:sticky; left:0; z-index:1;
  background:var(--surface);
  min-width:12rem;
  border-right:1px solid var(--line);
  white-space:normal;
}
.rate-table tbody tr:hover th:first-child,
.rate-table tbody tr:hover td{ background:var(--surface-2); }

.rental-name{ display:block; font-weight:600; color:var(--text); }
.rental-spec,
.rental-requires,
.rental-note{ display:block; font-size:var(--body-xs); color:var(--muted); }
.rental-requires{ color:var(--accent-text); }

.rate{ font-variant-numeric:tabular-nums; }
/* The em dash means "not offered at this period" — real information, not
   decoration. At opacity .6 it measured 3.37:1 on --surface. .8 gives
   4.96:1, and 4.62:1 on the --surface-2 row-hover state. */
.rate-na{ color:var(--muted); opacity:.8; }
.rate-gated{
  font-family:var(--font-display);
  font-size:var(--body-xs); text-transform:uppercase; letter-spacing:.06em;
  color:var(--accent-text);
}

/* Plain content table (hours-style lists, spec tables) — same shell, no
   sticky column. */
.data-table{ font-size:var(--body-sm); }
.data-table th,
.data-table td{ padding:.7rem .9rem; border-bottom:1px solid var(--line); text-align:left; }
.data-table thead th{
  font-family:var(--font-display); text-transform:uppercase;
  letter-spacing:.08em; font-size:var(--body-xs);
}


/* ---------------------------------------------------------- breadcrumbs -- */

.breadcrumbs{ font-size:var(--body-xs); color:var(--muted); margin-bottom:1.25rem; }
.breadcrumbs ol{ display:flex; flex-wrap:wrap; align-items:center; gap:.35rem; }
.breadcrumbs li{ display:flex; align-items:center; gap:.35rem; }
.breadcrumbs li + li::before{ content:"/"; opacity:.45; }
.breadcrumbs a:hover{ color:var(--accent-text); }
.breadcrumbs [aria-current]{ color:var(--text); }


/* --------------------------------------------------------- misc surface -- */

.panel{
  background:var(--surface);
  border:var(--border);
  border-radius:var(--card-radius);
  padding:clamp(1.15rem,3vw,2rem);
}
.panel--accent{ border-left:3px solid var(--accent); }

/* --accent-bright is the focus ring, and it is tuned for the charcoal base. On
   a light band it disappears: #e6e8ea on #f4f4f2 is 1.12:1, safety yellow
   #FFC94D on white is 1.53:1, Bobcat #FF8A33 on white is 2.35:1 — all under
   the 3:1 WCAG 2.2 needs for a focus indicator, and the neutral one is simply
   invisible. --accent-deep is the same hue picked for a light background and
   ranges 6.8:1 to 9.3:1 across the four themes.

   Re-pointing the TOKEN rather than writing a :focus-visible rule is what makes
   this total: everything that reads var(--focus-ring) — .table-scroll,
   .eq-jump, the hamburger, every form field — inherits the fix for free. */
.on-light{ --focus-ring:0 0 0 3px var(--accent-deep); }

.on-light{ background:var(--bg-light); color:var(--on-light); }
.on-light h1,.on-light h2,.on-light h3,.on-light h4{ color:var(--on-light); }
.on-light .card{ background:#fff; border-color:var(--line-light); }
.on-light .card-blurb,.on-light .card-meta{ color:var(--muted-light); }

.lede{ font-size:1.075rem; color:var(--muted); max-width:64ch; }
.on-light .lede{ color:var(--muted-light); }

/* ========================================================== FILTER CHROME ==
   Driven by js/20-filter.js. Read that file's header before changing anything
   here — the split between what ships in the HTML and what is created at
   runtime is deliberate, and it is what keeps the no-JS page correct.

   WHAT SHIPS IN THE HTML          WHAT js/20-filter.js CREATES
     the jump-nav <a href="#cat-x">  the <button> that replaces each one
     <p class="filter-count">        the "All" button
     every item, every section       the <input type="search"> + its label
                                     the "Clear" button
                                     the empty-result message

   A search box that cannot search is worse than no search box, so it does not
   exist until something can drive it. The anchors ship because an anchor with
   no JS still does its job — it jumps.
-------------------------------------------------------------------------- */

/* Belt to that braces. The bar is inserted into the DOM before the first
   filter pass runs; data-filter-ready is set only AFTER it succeeds. If
   20-filter.js throws in between, this keeps the half-built control off the
   screen instead of leaving a dead input behind. */
[data-filter-root]:not([data-filter-ready]) .filter-search{ display:none; }

.filter-search{
  display:flex;
  flex-wrap:wrap;
  align-items:center;
  gap:.6rem;
  margin-bottom:1.1rem;
}
.filter-search-label{
  font-family:var(--font-display);
  font-size:var(--eyebrow-size); font-weight:600;
  text-transform:uppercase; letter-spacing:var(--tracking-eyebrow);
  color:var(--accent-text);
}
.on-light .filter-search-label{ color:var(--accent-deep); }

.filter-search-input{
  flex:1 1 16rem;
  min-width:0;              /* without this a flex item refuses to shrink below
                               its intrinsic width and pushes the page sideways
                               at 375px */
  max-width:26rem;
  min-height:44px;
  padding:.55rem .8rem;
  background:var(--bg-2);
  color:var(--text);
  border:1px solid var(--line-strong);
  border-radius:var(--btn-radius);
  font-size:var(--body-sm);
  transition:border-color var(--t);
}
.filter-search-input::placeholder{ color:var(--muted); opacity:1; }
.filter-search-input:hover,
.filter-search-input:focus{ border-color:var(--accent); }
.on-light .filter-search-input{
  background:#fff; color:var(--on-light); border-color:var(--line-light);
}

.filter-clear{
  min-height:44px;
  padding:.5rem .9rem;
  background:transparent;
  color:var(--muted);
  border:1px solid var(--line-strong);
  border-radius:var(--btn-radius);
  font-family:var(--font-display);
  font-size:var(--body-xs); font-weight:600;
  text-transform:uppercase; letter-spacing:.08em;
  cursor:pointer;
  transition:color var(--t),border-color var(--t);
}
.filter-clear:hover,
.filter-clear:focus-visible{ color:var(--accent-text); border-color:var(--accent); }

/* The rewritten jump-nav buttons keep every class their anchor had, so the
   page's own chrome styling still applies. These three declarations are the
   only things a <button> needs that an <a> did not. */
.filter-btn{
  cursor:pointer;
  text-align:inherit;
  -webkit-appearance:none;
  appearance:none;
}

.filter-empty{
  margin-top:1.25rem;
  padding:1rem 1.15rem;
  border:var(--border);
  border-left:3px solid var(--accent);
  border-radius:var(--card-radius);
  background:var(--surface);
  color:var(--muted);
  font-size:var(--body-sm);
}
.filter-empty:focus-visible{ box-shadow:var(--focus-ring); }

/* Result counter. Hidden until JS writes into it, so a no-JS visitor never
   sees an empty "showing of". */
.filter-count:empty{ display:none; }
.filter-count{
  font-size:var(--body-sm); color:var(--muted);
  margin-top:.75rem;
}
.on-light .filter-count,
.on-light .filter-empty{ color:var(--muted-light); }

/* The one class js/20-filter.js toggles. !important because it has to beat
   .card{display:flex} and any per-page display a fragment sets on a section —
   same reasoning as the [hidden] rule below it, which predates this. */
.is-filtered-out{ display:none !important; }
[data-item][hidden]{ display:none !important; }

@media (prefers-reduced-motion:reduce){
  .card,.card-media img,.btn{ transition:none; }
  .card:hover .card-media img{ transform:none; }
}

/* ── styles/40-forms.css ── */
/* ============================================================================
   Forms — inputs, labels, selects, radios, checkboxes, states, status region

   BREAKPOINTS: 760px and 1024px. There is no third one.

   Every form on this site is a real <form method="POST"> to Web3Forms, so all
   of this has to look and behave right with JavaScript switched off. Nothing
   below depends on a script running.
   ========================================================================== */


/* ------------------------------------------------------------ the shell -- */

.w3form{
  background:var(--surface);
  border:var(--border);
  border-radius:var(--card-radius);
  padding:clamp(1.15rem,3vw,2rem);
}
.form-intro{ color:var(--muted); margin-bottom:1.5rem; max-width:60ch; }

/* One column on a phone; two from the tablet breakpoint, where a field opts
   in with .field--half. .field--full and fieldsets always span both. */
.form-grid{ display:grid; gap:1.15rem; grid-template-columns:1fr; }
@media (min-width:760px){
  .form-grid{ grid-template-columns:repeat(2,minmax(0,1fr)); }
  .form-grid > .field{ grid-column:1 / -1; }
  .form-grid > .field--half{ grid-column:span 1; }
}

.form-actions{ margin-top:1.5rem; display:flex; flex-wrap:wrap; gap:.75rem; align-items:center; }
.form-actions .btn{ width:100%; }
@media (min-width:760px){
  .form-actions .btn{ width:auto; }
}


/* --------------------------------------------------------------- field -- */

.field{ display:flex; flex-direction:column; gap:.4rem; min-width:0; }

.field label,
.field legend{
  font-family:var(--font-display);
  font-size:var(--body-sm); font-weight:500;
  text-transform:uppercase; letter-spacing:.06em;
  color:var(--text);
}

/* Required is marked twice on purpose: the asterisk for sighted users, the
   `required` attribute + a visually-hidden "(required)" for everyone else.
   The asterisk alone is decoration, not information. */
.req{ color:var(--accent-text); margin-left:.2rem; }

.field-help{ font-size:var(--body-xs); color:var(--muted); }


/* ------------------------------------------------------------- controls -- */

.field input,
.field textarea,
.field select{
  width:100%;
  padding:.7rem .8rem;
  background:var(--bg-2);
  color:var(--text);
  border:1px solid var(--line-strong);
  border-radius:var(--btn-radius);
  font-size:var(--body);
  transition:border-color var(--t),background var(--t);
}
/* 16px minimum on the actual control, or iOS Safari zooms the page on focus
   and the visitor is left scrolled sideways mid-form. --body is 1rem. */

.field textarea{ resize:vertical; min-height:7rem; line-height:var(--leading); }

.field input::placeholder,
.field textarea::placeholder{ color:var(--muted); opacity:.85; }
/* .65 composited to roughly 3.8:1 on --bg-2. Placeholder text is text, and
   1.4.3 applies to it; .85 measures about 5.5:1. The hint is still visibly
   lighter than a real value, which is all the dimming was ever for. */

.field input:hover,
.field textarea:hover,
.field select:hover{ border-color:var(--muted); }

.field input:focus-visible,
.field textarea:focus-visible,
.field select:focus-visible{
  outline:none;
  border-color:var(--accent);
  box-shadow:var(--focus-ring);
  background:var(--surface);
}

.field input:disabled,
.field textarea:disabled,
.field select:disabled{ opacity:.5; cursor:not-allowed; }

/* Date/time pickers render their indicator dark-on-dark in Chromium. */
.field input[type="date"]::-webkit-calendar-picker-indicator,
.field input[type="time"]::-webkit-calendar-picker-indicator{ filter:invert(1); opacity:.6; }
.field input[type="date"],
.field input[type="time"]{ min-height:2.9rem; }


/* -------------------------------------------------------------- select -- */

.select-wrap{ position:relative; }
.select-wrap select{
  appearance:none;
  padding-right:2.4rem;
  cursor:pointer;
}
.select-wrap::after{
  content:""; position:absolute; right:1rem; top:50%;
  width:.5rem; height:.5rem;
  border-right:2px solid var(--muted);
  border-bottom:2px solid var(--muted);
  transform:translateY(-70%) rotate(45deg);
  pointer-events:none;
}
.select-wrap select option,
.select-wrap select optgroup{ background:var(--surface); color:var(--text); }
.select-wrap select optgroup{ font-style:normal; font-weight:600; }


/* ------------------------------------------------ radios and checkboxes -- */

.field--group{ border:0; padding:0; margin:0; min-width:0; }
.field--group legend{ padding:0; margin-bottom:.5rem; }

.choice-list{ display:flex; flex-direction:column; gap:.5rem; }
.field--inline .choice-list{ flex-direction:row; flex-wrap:wrap; gap:.5rem 1.25rem; }

.choice{ display:flex; align-items:flex-start; gap:.6rem; }

.choice input[type="radio"],
.choice input[type="checkbox"]{
  appearance:none;
  flex:0 0 auto;
  width:1.15rem; height:1.15rem;
  margin-top:.22rem;
  background:var(--bg-2);
  border:1px solid var(--line-strong);
  cursor:pointer;
  transition:background var(--t),border-color var(--t);
}
.choice input[type="radio"]{ border-radius:50%; }
.choice input[type="checkbox"]{ border-radius:var(--btn-radius); }

.choice input:hover{ border-color:var(--accent); }
.choice input:checked{ background:var(--accent); border-color:var(--accent); }

/* The tick / dot is drawn with a box-shadow inset so it inherits --accent-ink
   and stays legible on orange, red and yellow alike. */
.choice input[type="radio"]:checked{
  box-shadow:inset 0 0 0 .28rem var(--accent-ink);
}
.choice input[type="checkbox"]:checked{
  background-image:linear-gradient(45deg,transparent 45%,var(--accent-ink) 45%,var(--accent-ink) 55%,transparent 55%),
                   linear-gradient(-45deg,transparent 45%,var(--accent-ink) 45%,var(--accent-ink) 55%,transparent 55%);
  background-size:.55rem .12rem,.85rem .12rem;
  background-position:24% 62%,52% 45%;
  background-repeat:no-repeat;
}

.choice label{
  font-family:var(--font-body);
  font-size:var(--body-sm); font-weight:400;
  text-transform:none; letter-spacing:0;
  color:var(--text);
  cursor:pointer;
}


/* ---------------------------------------------------- validation states -- */
/* :user-invalid, not :invalid — :invalid paints every required field red the
   moment the page loads, before anyone has typed a thing. */

.field input:user-invalid,
.field textarea:user-invalid,
.field select:user-invalid{
  border-color:#e5484d;
  background:rgba(229,72,77,.06);
}
.field .field-error{ font-size:var(--body-xs); color:#ff6369; }

.field.is-valid input,
.field.is-valid textarea,
.field.is-valid select{ border-color:#30a46c; }


/* -------------------------------------------------------- status region -- */
/* role="status" aria-live="polite" lives on this element. It is EMPTY until
   the JS path writes into it, so nothing is announced on page load and the
   no-JS path (a full POST to /thank-you) never touches it at all. */

.form-status{ margin-top:1rem; font-size:var(--body-sm); }
.form-status:empty{ display:none; }

.form-status[data-state="sending"]{ color:var(--muted); }
.form-status[data-state="ok"]{
  color:#30a46c;
  border-left:3px solid #30a46c;
  padding:.6rem .9rem;
  background:rgba(48,164,108,.08);
}
.form-status[data-state="error"]{
  color:#ff6369;
  border-left:3px solid #e5484d;
  padding:.6rem .9rem;
  background:rgba(229,72,77,.08);
}


/* ----------------------------------------------------- forms on a light -- */

.on-light .w3form{ background:#fff; border-color:var(--line-light); }
.on-light .field label,
.on-light .field legend,
.on-light .choice label{ color:var(--on-light); }
.on-light .field input,
.on-light .field textarea,
.on-light .field select{
  background:var(--bg-light);
  color:var(--on-light);
  border-color:var(--line-light);
}
.on-light .field-help,
.on-light .form-intro{ color:var(--muted-light); }
.on-light .req{ color:var(--accent-deep); }

/* ── styles/41-page-forms.css ── */
/* ============================================================================
   Form PAGES — the shell around the form, not the controls themselves.

   40-forms.css owns inputs, labels, selects, choices, validation and the status
   region. This file owns the page: the form column + aside layout used by
   /contact, /equipment/get-pricing, /rentals/availability and
   /boss-snowplows/quote, plus /thank-you.

   BREAKPOINTS: 760px and 1024px. There is no third one.

   TOUCH TARGETS: these visitors are on a phone, in a truck, often with gloves
   on. Everything tappable on a form page is bumped to a comfortable size here
   rather than in 40-forms.css, so the sitewide defaults stay untouched.
   ========================================================================== */


/* ------------------------------------------------------------- page shell -- */

.form-layout{
  display:grid;
  gap:var(--gap);
  grid-template-columns:1fr;
  align-items:start;
}
@media (min-width:1024px){
  .form-layout{ grid-template-columns:minmax(0,1.45fr) minmax(0,.85fr); gap:clamp(1.75rem,3vw,3rem); }
}

.form-main{ min-width:0; }

/* Small print under the form. Deliberately quiet — it is reassurance, not a
   term of business. */
.form-legal{
  margin-top:1rem;
  font-size:var(--body-xs);
  color:var(--muted);
  max-width:58ch;
}


/* ------------------------------------------------------------------ aside -- */
/* Stacks under the form on a phone. The header is sticky, so the sticky offset
   has to clear it or the panel tucks underneath the nav bar. */

.form-aside{ min-width:0; display:flex; flex-direction:column; gap:var(--gap); }

@media (min-width:1024px){
  .form-aside{
    position:sticky;
    top:calc(var(--nav-h) + 1.25rem);
  }
}

.side-head{
  font-size:var(--heading-3);
  margin-bottom:.6rem;
}
.side-lead{ color:var(--muted); font-size:var(--body-sm); }
.side-actions{ margin-top:1.1rem; }
.side-actions .btn{ width:100%; }
.side-note{
  margin-top:1rem;
  font-size:var(--body-sm);
  color:var(--muted);
}
/* Underlined, not just recoloured: on a neutral page (/contact, /thank-you)
   --accent is a near-white grey and colour alone would not read as a link. */
.side-note a{
  color:var(--accent-text);
  text-decoration:underline;
  text-underline-offset:.2em;
}
.side-note a:hover{ color:var(--accent-bright); }

/* The NAP block sits inside a panel here, where the footer's muted greys read
   as washed out. */
.form-aside .nap{ margin-top:1.1rem; }
.form-aside .hours{ max-width:none; }
.form-aside .map-embed iframe{ height:260px; }


/* ------------------------------------------------------------- trust list -- */
/* Short reassurance bullets. The dash marker matches .specs so the two read as
   one family. */

.trust-list{ display:flex; flex-direction:column; gap:.7rem; }
.trust-list li{
  position:relative;
  padding-left:1.15rem;
  font-size:var(--body-sm);
  color:var(--muted);
}
.trust-list li::before{
  content:"";
  position:absolute; left:0; top:.62em;
  width:6px; height:2px;
  background:var(--accent);
}
.trust-list strong{ color:var(--text); font-weight:600; }

/* When the bullets are links they are navigation, so they get a real target
   height rather than a text-sized one. */
.trust-list--links li a{
  display:block;
  padding:.35rem 0;
  color:var(--text);
  font-size:var(--body);
}
.trust-list--links li a:hover{ color:var(--accent-text); }


/* ------------------------------------------------- touch targets on forms -- */
/* 2.9rem+ on every control, and the whole choice row is the tap target rather
   than the 1.15rem box or the label text alone. */

.form-page .field input,
.form-page .field select{ min-height:3rem; padding:.8rem .9rem; }
.form-page .field textarea{ padding:.8rem .9rem; }
.form-page .select-wrap select{ padding-right:2.6rem; }

.form-page .choice-list{ gap:.35rem; }

.form-page .choice{
  align-items:center;
  gap:.75rem;
  min-height:3rem;
  padding:.4rem .75rem;
  background:var(--bg-2);
  border:1px solid var(--line);
  border-radius:var(--btn-radius);
  transition:border-color var(--t),background var(--t);
}
.form-page .choice:hover{ border-color:var(--line-strong); }

.form-page .choice input[type="radio"],
.form-page .choice input[type="checkbox"]{
  width:1.35rem; height:1.35rem;
  margin-top:0;
  background:var(--surface);
}

/* flex:1 makes the <label> fill the row, so the tap lands on the control even
   when the text is one short word. */
.form-page .choice label{
  flex:1 1 auto;
  padding-block:.35rem;
  font-size:var(--body);
}

/* A row containing a checked control reads as selected at a glance — useful on
   the 10-option pricing radio group where the dot alone is easy to miss. */
.form-page .choice:has(input:checked){
  border-color:var(--accent);
  background:var(--surface);
}

/* Set by js/10-forms.js when the pricing form is preselected from
   ?model=<family-id>, so the visitor can see we already answered the question
   for them. No :has() dependency — this one has to be reliable. */
.form-page .choice.is-prefilled{
  border-color:var(--accent);
  background:var(--surface);
}

/* Room to breathe between the model question and the contact details. */
.form-page .field--group{ padding-block:.25rem; }


/* --------------------------------------------------------- /thank-you ------ */

.thanks-head{ max-width:60ch; }
.thanks-head h1{ margin-bottom:.75rem; }

.thanks-call{
  margin-top:clamp(1.75rem,4vw,2.5rem);
  text-align:left;
}
.thanks-call-copy{
  font-family:var(--font-display);
  font-size:var(--heading-3);
  text-transform:uppercase;
  letter-spacing:.04em;
  color:var(--text);
}
.thanks-call-action{ margin-top:1rem; }
.thanks-call-action .btn{ width:100%; }
@media (min-width:760px){
  .thanks-call-action .btn{ width:auto; }
}

.thanks-more{ margin-top:clamp(2.5rem,6vw,4rem); }
.thanks-more h2{ margin-bottom:clamp(1.25rem,3vw,1.75rem); }
.thanks-more .card-foot{ padding-top:0; }

/* wrap-narrow caps the copy, but three pillar cards want the room back. */
@media (min-width:760px){
  .thanks-more .grid--3{ grid-template-columns:repeat(3,minmax(0,1fr)); }
}

/* ── styles/50-page-equipment.css ── */
/* ============================================================================
   50-page-equipment.css — /equipment (the Equipment Sales pillar)

   OWNS: only what templates/pages/equipment.html renders. Every selector here
   is either an .eq-* class that exists nowhere else, or is scoped under
   body.theme-equipment. Nothing in this file can reach another page.

   BREAKPOINTS: 760px and 1024px. There is no third one and there must not be.

   COLOUR: var() only, no hex. --accent is Bobcat orange because
   <body class="theme-equipment"> re-points it in 00-tokens.css. If you find
   yourself typing #F47216, the theme system is already doing it for you.

   IMAGE SIZING IS DELIBERATE — read before changing .eq-grid .card-media:
   three of the twelve product shots are small and soft
   (bobcat-uv34xl 330x220, bobcat-zt2048 404x327, bobcat-ct2535 543x362).
   The card media is 3/2, not the 4/3 default, because 330x220 and 543x362 are
   EXACTLY 3/2 — so those two crop not at all and upscale by ~13% at the widest
   card instead of ~27%. Widening the grid or restoring 4/3 makes them mush.
   ========================================================================== */

body.theme-equipment{
  /* Page-local knobs. Motion distance and the hero's minimum height are the
     only two numbers this page invents; both live here so they are changed
     once. Everything else is a global token. */
  --eq-rise:1.25rem;
  --eq-hero-min:clamp(26rem,72vh,40rem);
}


/* =============================================================== HERO ==== */
/* Photo behind, copy over the bottom. The machines in hero-equipment.webp sit
   across the middle of the frame and the bottom third is open asphalt, which
   is why --hero-overlay (transparent at the top, near-solid by 78%) lands the
   text on empty ground rather than across a mower. */

.eq-hero{
  position:relative;
  display:flex;
  align-items:flex-end;
  min-height:var(--eq-hero-min);
  padding-block:0;
  overflow:hidden;
  border-bottom:1px solid var(--line);
}

.eq-hero-media{
  position:absolute;
  inset:0;
  z-index:0;
}
.eq-hero-media img{
  width:100%;
  height:100%;
  object-fit:cover;
  object-position:center;
}
/* The overlay is a separate layer rather than a gradient on the media box so
   the image keeps its own stacking context and never bleeds over the copy. */
.eq-hero-media::after{
  content:"";
  position:absolute;
  inset:0;
  background:var(--hero-overlay);
}

.eq-hero-inner{
  position:relative;
  z-index:1;
  padding-block:clamp(2.5rem,7vw,4.5rem);
}

.eq-hero-inner .breadcrumbs{ margin-bottom:1rem; }

.eq-hero h1{ max-width:18ch; }

.eq-hero-lede{
  margin-top:1rem;
  color:var(--text);
  opacity:.88;
}

.eq-hero-actions{ margin-top:1.75rem; }

@media (min-width:1024px){
  .eq-hero-inner{ padding-block:clamp(3.5rem,6vw,5.5rem); }
  .eq-hero-lede{ max-width:56ch; }
}


/* ============================================================== PROMO ==== */
/* An accent-tinted band, not a solid accent fill: the closing .cta-band is
   already a solid orange slab and two of them on one page reads as a warning,
   not an offer. color-mix keeps this derived from --accent, so it re-themes
   with everything else and hard-codes nothing. */

.eq-promo{
  background:var(--bg-2);
  background:color-mix(in srgb, var(--accent) 9%, var(--bg));
  border-block:1px solid var(--line);
}

.eq-promo-panel{
  display:flex;
  flex-direction:column;
  gap:1.5rem;
  padding:clamp(1.25rem,4vw,2.25rem);
  background:var(--surface);
  border:var(--border);
  border-left:3px solid var(--accent);
  border-radius:var(--card-radius);
}

.eq-promo-head{
  font-size:var(--heading-2);
  text-wrap:balance;
}

.eq-promo-sub{
  margin-top:.6rem;
  color:var(--muted);
}

/* The expiry is not fine print. It is the only thing on the page that stops a
   stale offer reading as a live one, so it gets accent weight, not grey. */
.eq-promo-ends{
  margin-top:.9rem;
  font-family:var(--font-display);
  font-size:var(--body-sm);
  text-transform:uppercase;
  letter-spacing:.08em;
  color:var(--accent);
}

.eq-promo-fine{
  margin-top:.85rem;
  font-size:var(--body-xs);
  color:var(--muted);
  opacity:.8;
}

.eq-promo-act .btn{ width:100%; }

@media (min-width:760px){
  .eq-promo-panel{
    flex-direction:row;
    align-items:center;
    justify-content:space-between;
    gap:var(--gap);
  }
  .eq-promo-copy{ flex:1 1 auto; }
  .eq-promo-act{
    flex:0 0 auto;
    max-width:20rem;
    text-align:right;
  }
  .eq-promo-act .btn{ width:auto; }
}


/* ============================================================= LINEUP ==== */

/* -------------------------------------------------------- jump nav ------ */
/* Real anchors today, filter buttons after a later pass. On a 375px screen
   five category names cannot wrap into anything readable, so the row scrolls
   sideways instead — and every chip stays a full 44px tap target. */

.eq-jump{
  margin-bottom:clamp(2rem,4vw,3rem);
  overflow-x:auto;
  -webkit-overflow-scrolling:touch;
  scrollbar-width:thin;
}
.eq-jump:focus-visible{ box-shadow:var(--focus-ring); }

.eq-jump-list{
  display:flex;
  gap:.5rem;
  padding-bottom:.5rem;
}
.eq-jump-list li{ flex:0 0 auto; }

.eq-jump-link{
  display:inline-flex;
  align-items:center;
  gap:.5rem;
  min-height:44px;
  padding:.5rem .9rem;
  background:var(--surface);
  border:1px solid var(--line-strong);
  border-radius:var(--btn-radius);
  font-family:var(--font-display);
  font-size:.82rem;
  font-weight:600;
  text-transform:uppercase;
  letter-spacing:.06em;
  white-space:nowrap;
  transition:color var(--t),border-color var(--t),background var(--t);
}
/* js/20-filter.js rewrites these anchors into <button>s once it loads. The
   class comes across with them, so everything above still applies; the pressed
   state below is the one thing an anchor never needed. */
.eq-jump-link:hover,
.eq-jump-link:focus-visible,
.eq-jump-link[aria-pressed="true"]{
  color:var(--accent);
  border-color:var(--accent);
  background:var(--surface-2);
}
.eq-jump-link[aria-pressed="true"]{ box-shadow:inset 0 -2px 0 0 var(--accent); }

.eq-jump-count{
  display:inline-flex;
  align-items:center;
  justify-content:center;
  min-width:1.6em;
  padding:.1rem .3rem;
  background:var(--bg-2);
  border-radius:var(--btn-radius);
  font-family:var(--font-body);
  font-size:var(--body-xs);
  font-weight:600;
  letter-spacing:0;
  color:var(--muted);
}
.eq-jump-link:hover .eq-jump-count,
.eq-jump-link:focus-visible .eq-jump-count,
.eq-jump-link[aria-pressed="true"] .eq-jump-count{ color:var(--accent); }

@media (min-width:760px){
  .eq-jump{ overflow-x:visible; }
  .eq-jump-list{ flex-wrap:wrap; padding-bottom:0; }
}

/* ------------------------------------------------------- category ------- */

.eq-cat{
  /* These are <section>s so the anchors land on a labelled region, which means
     the global section{padding-block:var(--section-y)} has to be undone —
     they are subsections, not page sections. */
  padding-block:0;
  margin-bottom:clamp(2.5rem,6vw,4rem);
}
.eq-cat:last-of-type{ margin-bottom:0; }

.eq-cat-head{
  margin-bottom:clamp(1rem,2.5vw,1.5rem);
  padding-bottom:.85rem;
  border-bottom:1px solid var(--line);
}

.eq-cat-title{
  font-size:var(--heading-3);
  text-transform:uppercase;
  letter-spacing:.05em;
}
/* A short accent rule under the heading — the only ornament on the page, and
   it is doing the job of telling five stacked category blocks apart. */
.eq-cat-title::after{
  content:"";
  display:block;
  width:2.5rem;
  height:3px;
  margin-top:.6rem;
  background:var(--accent);
}

.eq-cat-blurb{
  margin-top:.75rem;
  max-width:68ch;
  color:var(--muted);
  font-size:var(--body-sm);
}

/* ----------------------------------------------------------- cards ------ */
/* See the header note: 3/2 is chosen from the actual pixel dimensions of the
   weakest three photos, not from taste. */

.eq-grid .card-media{ aspect-ratio:3/2; }

/* The UT6566 renders a .ph tile instead of its watermarked photo. Give it the
   same height as a real card's media so the row does not go ragged. */
.eq-grid .card-media .ph{ min-height:0; }

.eq-grid .card{ border-top:3px solid transparent; }
.eq-grid .card:hover,
.eq-grid .card:focus-within{
  border-top-color:var(--accent);
  border-color:var(--line-strong);
}

/* Two studio shots (UV34 XL, ZT2048) sit on near-white backgrounds and glare
   against the charcoal cards. A hairline under the media closes the seam. */
.eq-grid .card-media{ border-bottom:1px solid var(--line); }

.eq-grid .card-foot .btn{ width:100%; }

@media (min-width:760px){
  .eq-grid .card-foot .btn{ width:auto; }
}

/* ------------------------------------------------------- lineup foot ---- */

.eq-lineup-foot{
  display:flex;
  flex-wrap:wrap;
  align-items:center;
  gap:1rem;
  margin-top:clamp(2.5rem,5vw,3.5rem);
  padding-top:clamp(1.5rem,3vw,2rem);
  border-top:1px solid var(--line);
  color:var(--muted);
  font-size:var(--body-sm);
}


/* ====================================================== GENERAC + DR ===== */
/* A light band, on purpose. logo-generac.png is a navy wordmark baked onto a
   SOLID WHITE background with no alpha channel — on the charcoal base it would
   render as a white rectangle, and blending the white away would be faking a
   transparency the file does not have. .on-light .card is already pure white
   in 30-components.css, so the logo's own background and the card's background
   are the same value and the box simply is not there. */

.eq-brand{ height:100%; }

.eq-brand-plate{
  display:flex;
  align-items:center;
  justify-content:center;
  min-height:7.5rem;
  padding:1.5rem;
  border-bottom:1px solid var(--line-light);
}
/* Cap by height, not width: the two logos have wildly different aspect ratios
   (209x64 wordmark vs 156x155 roundel) and matching their optical weight means
   matching their height. */
.eq-brand-plate img{
  width:auto;
  max-height:3.5rem;
  object-fit:contain;
}

.eq-brand-title{
  font-size:var(--heading-3);
  text-transform:uppercase;
  letter-spacing:.05em;
}

.eq-brand-copy{
  margin-top:.85rem;
  color:var(--muted-light);
  font-size:var(--body-sm);
}

.eq-brand .specs{ margin-top:1.1rem; }

.eq-brand-foot{
  display:flex;
  flex-wrap:wrap;
  gap:.6rem;
}
.eq-brand-foot .btn{ flex:1 1 auto; }

@media (min-width:760px){
  .eq-brand-foot .btn{ flex:0 0 auto; }
}


/* ========================================================= CROSS-LINKS === */

.eq-next-card{
  display:flex;
  flex-direction:column;
  gap:.4rem;
  height:100%;
  padding:clamp(1.15rem,3vw,1.6rem);
  background:var(--surface);
  border:var(--border);
  border-left:3px solid var(--line-strong);
  border-radius:var(--card-radius);
  transition:border-color var(--t),background var(--t);
}
.eq-next-card:hover,
.eq-next-card:focus-visible{
  background:var(--surface-2);
  border-color:var(--line-strong);
  border-left-color:var(--accent);
  color:var(--text);
}

.eq-next-kicker{
  font-family:var(--font-display);
  font-size:var(--eyebrow-size);
  font-weight:600;
  text-transform:uppercase;
  letter-spacing:var(--tracking-eyebrow);
  color:var(--accent);
}

.eq-next-title{
  font-family:var(--font-display);
  font-size:var(--heading-3);
  font-weight:600;
  line-height:1.1;
  color:var(--heading-color);
}

.eq-next-body{
  color:var(--muted);
  font-size:var(--body-sm);
}


/* ============================================================= MOTION ==== */
/* Drives the existing site/anim.js — this page ships no JavaScript of its own.
   anim.js observes [data-reveal] and [data-stagger], then adds .in (to the
   element itself, or to each direct child for a stagger). All this file does
   is describe the before and after.

   TWO SAFETY GATES, both deliberate:
   1. @media (scripting:enabled) — if JS is switched off nothing is ever
      hidden. base.html hard-codes class="anim" on <html>, so without this gate
      a no-JS visitor would get a permanently blank page.
   2. html.anim — anim.js REMOVES that class from its own catch block, so a
      runtime error inside the animation code also reveals everything.
   A browser too old for the scripting media feature drops the block entirely
   and every element renders visible. Every failure mode fails to "shown". */

@media (scripting:enabled){
  html.anim body.theme-equipment [data-reveal],
  html.anim body.theme-equipment [data-stagger] > *{
    opacity:0;
    transform:translateY(var(--eq-rise));
    transition:opacity var(--t-slow),transform var(--t-slow);
  }
  html.anim body.theme-equipment [data-reveal].in,
  html.anim body.theme-equipment [data-stagger] > .in{
    opacity:1;
    transform:none;
  }
}

@media (prefers-reduced-motion:reduce){
  html.anim body.theme-equipment [data-reveal],
  html.anim body.theme-equipment [data-stagger] > *{
    opacity:1;
    transform:none;
    transition:none;
  }
  body.theme-equipment .eq-jump-link,
  body.theme-equipment .eq-next-card{ transition:none; }
}

/* ── styles/51-page-boss.css ── */
/* ============================================================================
   BOSS pillar + the ten category pages
   templates/pages/boss.html · templates/pages/boss-category.html

   BREAKPOINTS: 760px and 1024px. There is no third one.
   COLOUR: var(--accent) only. <body class="theme-boss"> re-points it to BOSS
   red (#EE312A) — no hex is written in this file and none ever should be.
   RADII: near-square, inherited from --card-radius / --btn-radius. BOSS forces
   border-radius:0 across their whole component set; do not round anything up.

   ── THE DESIGN PROBLEM THIS FILE SOLVES ─────────────────────────────────────
   There is NO BOSS imagery. Every image and hero_image in data/boss.json is
   null and that is deliberate — official artwork comes from the BOSS dealer
   portal later, and hotlinking bossplow.com or the Toro CDN is not an option.

   So the visual system here is TYPE AND RULES, not photography:
     · one real photograph (the storefront) anchors the top of every page —
       full-bleed on the pillar, a 12%-opacity grayscale texture behind the
       category page heads;
     · 2px accent rules mark every structural edge — hero bottom, page-head
       bottom, card tops, step tops, the fleet table's top rule;
     · Oswald numerals (01–10) and uppercase eyebrows carry the rhythm that
       images would normally carry;
     · one light band (the by-vehicle index) breaks the charcoal so the page
       reads as designed rather than as a dark page waiting for pictures.

   The macros still emit cards.ph() with data-assets-pending="true", so the
   day the artwork lands one querySelectorAll finds every tile. What this file
   does is refuse to draw an EMPTY 4:3 photo frame ten times over — see the
   ARTWORK-PENDING block below, which is the one thing to delete when the real
   images arrive.
   ========================================================================== */


/* ══ ARTWORK-PENDING ═══════════════════════════════════════════════════════
   DELETE THIS ONE BLOCK WHEN BOSS ARTWORK LANDS.
   The <img> path is already wired inside cards.boss_category_card and
   cards.boss_model_card; the moment `image` stops being null in boss.json,
   removing these three rules restores the 4:3 media frame with no template
   change. Until then: ten identical hatched tiles read as a broken gallery,
   one hatched tile per model card reads as a skeleton loader, and neither
   reads as a finished page. A card driven by type and a red rule does.
   The placeholder markup stays in the DOM either way. */

.bs-cat-grid .card-media,
.bs-model-grid .card-media{ display:none; }

/* The accent rule takes over the job the photo was doing: it gives the card a
   top edge and the grid a repeating beat. */
.bs-cat-grid .cat-card,
.bs-model-grid .model-card{ border-top:2px solid var(--accent); }

/* .card:hover sets all four border colours and would grey out that red rule.
   Higher specificity, and it brightens instead. */
.bs-cat-grid .cat-card:hover,
.bs-model-grid .model-card:hover{ border-top-color:var(--accent-bright); }
/* ══ end ARTWORK-PENDING ═══════════════════════════════════════════════════ */


/* ------------------------------------------------------------------ hero -- */
/* Pillar page only. The storefront photo is the single piece of real imagery
   on the pillar, so it gets the whole top of the page. */

.bs-hero{
  position:relative;
  overflow:hidden;
  padding-block:clamp(3rem,8vw,6rem);
  border-bottom:2px solid var(--accent);
  background:var(--bg-2);
}
.bs-hero-img{
  position:absolute; inset:0;
  width:100%; height:100%;
  object-fit:cover;
  z-index:0;
  /* Below 1024 the copy spans the full width and sits ON the photo — the
     storefront sign lands directly behind the h1 and eats it. So the photo
     drops back to atmosphere on small screens and only comes up to full
     strength once the copy is confined to the left column. */
  opacity:.4;
}
/* Two layers: the shared vertical hero overlay, plus a horizontal wall from the
   page background so the copy stays legible over the bright part of the photo. */
.bs-hero-shade{
  position:absolute; inset:0; z-index:1;
  background:
    linear-gradient(90deg,var(--bg) 0%,var(--bg) 42%,transparent 100%),
    var(--hero-overlay);
}
.bs-hero-inner{
  position:relative; z-index:2;
  max-width:62ch;
}
.bs-hero-lede{ margin-top:1rem; color:var(--text); opacity:.9; }
.bs-hero .btn-pair{ margin-top:1.75rem; }

@media (min-width:1024px){
  .bs-hero{ padding-block:clamp(4rem,7vw,7rem); }
  .bs-hero-img{ opacity:1; }
  .bs-hero-shade{
    background:
      linear-gradient(90deg,var(--bg) 0%,var(--bg) 18%,transparent 82%),
      var(--hero-overlay);
  }
}


/* ------------------------------------------------------------- page head -- */
/* Category pages. No BOSS photo exists, so the storefront runs behind the
   heading at 12% and grayscale — texture, not a picture of the wrong thing. */

.bs-page-head{
  position:relative;
  overflow:hidden;
  background:var(--bg-2);
  border-bottom:2px solid var(--accent);
  padding-block:clamp(1.75rem,5vw,3rem);
}
.bs-page-head::before{
  content:"";
  /* inset is negative so the blur's soft edge falls outside the box instead of
     fading to a visible border on all four sides. */
  position:absolute; inset:-8%;
  background-image:url("/images/hero-storefront.webp");
  background-size:cover;
  background-position:center;
  opacity:.09;
  filter:grayscale(1) blur(4px);
}
/* Scrim over the photo. Blurred, desaturated and at 9% it has to read as
   texture — the moment you can make out a face or a sign behind the headline
   it stops looking deliberate and starts looking like a mistake. */
.bs-page-head::after{
  content:"";
  position:absolute; inset:0;
  background:linear-gradient(90deg,var(--bg-2) 0%,transparent 68%);
}
.bs-page-head > *{ position:relative; z-index:1; }
.bs-page-head-inner{ max-width:70ch; }
.bs-page-head .lede{ margin-top:.85rem; }

.bs-h1-geo{
  display:block;
  margin-top:.25rem;
  font-size:.5em;
  color:var(--muted);
  letter-spacing:.04em;
}
.bs-head-badges{ margin-top:1.1rem; }
.bs-page-head .btn-pair{ margin-top:1.25rem; }

@media (min-width:760px){
  .bs-h1-geo{ display:inline; margin-top:0; font-size:.42em; }
}


/* ------------------------------------------------------------------ band -- */

.bs-band{ background:var(--bg-2); }


/* ------------------------------------------------------------ proof band -- */

.bs-proof{
  background:var(--surface);
  border:var(--border);
  border-top:2px solid var(--accent);
  border-radius:var(--card-radius);
  padding:1.15rem;
}
.bs-proof-title{
  font-size:var(--heading-3);
  text-transform:uppercase;
  letter-spacing:.03em;
}
.bs-proof-body{ margin-top:.55rem; color:var(--muted); font-size:var(--body-sm); }


/* ----------------------------------------------------------- proof strip -- */
/* The compact version of the proof band, repeated on all ten category pages.
   Small solid accent squares instead of icons — nothing to load, nothing to
   licence, and it matches BOSS's blunt square-cornered language. */

.bs-proof-strip{
  background:var(--bg-2);
  border-bottom:1px solid var(--line);
}
.bs-proof-strip ul{
  display:flex; flex-wrap:wrap;
  gap:.6rem 1.6rem;
  padding-block:.9rem;
}
.bs-proof-strip li{
  display:flex; align-items:center; gap:.55rem;
  font-family:var(--font-display);
  font-size:var(--body-xs); font-weight:600;
  text-transform:uppercase; letter-spacing:.12em;
  color:var(--muted);
}
.bs-proof-strip li::before{
  content:"";
  flex:0 0 auto;
  width:8px; height:8px;
  background:var(--accent);
}


/* ------------------------------------------------------------ advantages -- */
/* Ten entries, rule-separated. The hairline grid IS the design — it is what
   photography would otherwise be doing. Entry 01 (LED standard) leads and
   spans both columns on desktop, because that is the order of emphasis. */

.bs-adv-list{ display:grid; grid-template-columns:1fr; }

.bs-adv{
  position:relative;
  padding:1.4rem 0 1.4rem 3.25rem;
  border-top:1px solid var(--line);
}
.bs-adv-num{
  position:absolute; left:0; top:1.35rem;
  font-family:var(--font-display);
  font-size:1.35rem; font-weight:600;
  letter-spacing:.02em;
  color:var(--accent-text);
}
.bs-adv-title{ font-size:var(--heading-3); }
.bs-adv-body{ margin-top:.5rem; color:var(--muted); max-width:64ch; }

@media (min-width:1024px){
  .bs-adv-list{ grid-template-columns:repeat(2,minmax(0,1fr)); column-gap:var(--gap); }
  .bs-adv:first-child{ grid-column:1 / -1; border-top:2px solid var(--accent); }
  .bs-adv:first-child .bs-adv-num{ font-size:1.9rem; top:1.25rem; }
  .bs-adv:first-child .bs-adv-title{ font-size:var(--heading-2); }
  .bs-adv:first-child .bs-adv-body{ font-size:1.075rem; max-width:80ch; }
}


/* -------------------------------------------------------- category cards -- */
/* With the empty media frame suppressed these become an index, not a gallery:
   title, blurb, model count pinned to the bottom, and a cue in the corner.
   .card-title a.stretched still makes the whole card one click target. */

.bs-cat-grid .cat-card{ padding-bottom:2.6rem; }
.bs-cat-grid .card-title{ text-transform:uppercase; letter-spacing:.02em; }
.bs-cat-grid .card-meta{
  margin-top:auto;
  padding-top:1rem;
  color:var(--accent-text);
}
.bs-cat-grid .cat-card::after{
  content:"View →";
  position:absolute; left:1.15rem; bottom:1rem;
  font-family:var(--font-display);
  font-size:var(--body-xs); font-weight:600;
  text-transform:uppercase; letter-spacing:.12em;
  color:var(--muted);
  pointer-events:none;
  transition:color var(--t),transform var(--t);
}
.bs-cat-grid .cat-card:hover::after{ color:var(--accent-text); transform:translateX(4px); }


/* ------------------------------------------------------------ model cards -- */

.bs-model-grid .card-title{ text-transform:uppercase; letter-spacing:.02em; }
.bs-model-grid .card-meta{ margin-top:1rem; }


/* ------------------------------------------------------- by-vehicle index -- */
/* The whole-fleet argument, made concrete: every vehicle type in the data,
   linked to the categories that cover it. On a light band, so the page has a
   contrast break that has nothing to do with having no photographs. */

.bs-fleet-dl{
  display:grid;
  border-top:2px solid var(--accent-deep);
}
.bs-fleet-row{
  display:grid; gap:.45rem;
  padding-block:.9rem;
  border-bottom:1px solid var(--line-light);
}
.bs-fleet-row dt{
  font-family:var(--font-display);
  font-size:var(--body-sm); font-weight:600;
  text-transform:uppercase; letter-spacing:.06em;
  color:var(--on-light);
}
.bs-fleet-row dd{ display:flex; flex-wrap:wrap; gap:.4rem; }

.bs-fleet-note{
  margin-top:1.5rem;
  max-width:70ch;
  font-size:var(--body-sm);
  color:var(--muted-light);
}
.bs-fleet-note a{ color:var(--accent-deep); font-weight:600; }
.bs-fleet-note a:hover{ color:var(--on-light); }

@media (min-width:760px){
  .bs-fleet-row{ grid-template-columns:13rem minmax(0,1fr); gap:1rem; align-items:baseline; }
}


/* ----------------------------------------------------------------- chips -- */
/* Dark by default; the .on-light override is what the fleet index uses.
   The reset sets a:hover{color:var(--accent-text)} at (0,1,1), so each of these
   needs its own hover colour or the accent leaks through. */

.bs-chip{
  display:inline-block;
  padding:.28rem .6rem;
  font-family:var(--font-display);
  font-size:var(--body-xs); font-weight:600;
  text-transform:uppercase; letter-spacing:.08em;
  color:var(--text);
  border:1px solid var(--line-strong);
  border-radius:var(--btn-radius);
  transition:color var(--t),border-color var(--t),background var(--t);
}
.bs-chip:hover,
.bs-chip:focus-visible{ color:var(--accent-text); border-color:var(--accent); }

.on-light .bs-chip{ color:var(--on-light); border-color:var(--line-light); background:transparent; }
.on-light .bs-chip:hover,
.on-light .bs-chip:focus-visible{
  color:var(--bg-light);
  background:var(--accent-deep);
  border-color:var(--accent-deep);
}


/* ------------------------------------------------------------- use case -- */

.bs-usecase{ margin-bottom:clamp(2rem,4vw,3rem); }
.bs-usecase p{ max-width:70ch; color:var(--text); }


/* --------------------------------------------------------- fitment table -- */
/* Published fitment only. A category whose models have no blade widths does not
   get a widths column full of em dashes — the column is not rendered at all and
   the table stays narrow enough that it never scrolls. Thin reads as precise;
   a half-empty table reads as unfinished. */

.bs-fit-head{
  margin-top:clamp(2rem,4vw,3rem);
  margin-bottom:1rem;
  font-size:var(--heading-3);
}
.bs-fit-table--wide{ min-width:34rem; }

.bs-fit-table tbody th{ vertical-align:top; }
.bs-fit-table td{ vertical-align:top; }

.bs-fit-model{
  display:block;
  font-family:var(--font-display);
  font-weight:600;
  text-transform:uppercase; letter-spacing:.03em;
  color:var(--text);
}
/* Part numbers are how a contractor actually orders. Quiet, but present. */
.bs-fit-parts{
  display:block;
  margin-top:.3rem;
  font-size:var(--body-xs);
  letter-spacing:.04em;
  color:var(--muted);
  opacity:.8;
}
.bs-fit-list{ display:flex; flex-direction:column; gap:.25rem; }
.bs-fit-list li{
  position:relative;
  padding-left:.9rem;
  font-size:var(--body-sm);
  color:var(--muted);
}
.bs-fit-list li::before{
  content:"";
  position:absolute; left:0; top:.62em;
  width:5px; height:2px;
  background:var(--accent);
}
.bs-fit-dims{ margin-top:0; }
/* 3.37:1 at .6 — same fix, same reason, as .rate-na in 30-components.css. */
.bs-fit-na{ color:var(--muted); opacity:.8; }


/* ----------------------------------------------------------------- steps -- */

.bs-steps{ display:grid; gap:var(--gap); grid-template-columns:1fr; }
.bs-step{ border-top:2px solid var(--accent); padding-top:1rem; }
.bs-step-num{
  font-family:var(--font-display);
  font-size:var(--body-xs); font-weight:600;
  text-transform:uppercase; letter-spacing:.14em;
  color:var(--accent-text);
}
.bs-step h3{ margin-top:.4rem; font-size:var(--heading-3); }
.bs-step p{ margin-top:.55rem; font-size:var(--body-sm); color:var(--muted); }

@media (min-width:760px){
  .bs-steps{ grid-template-columns:repeat(3,minmax(0,1fr)); }
}


/* ------------------------------------------------------------- quote CTA -- */
/* Deliberately dark and square, because partials/cta-band.html lands right
   underneath it as a solid accent fill. Two red bands in a row would be noise;
   dark panel then red band is a rhythm. */

.bs-quote{ display:flex; flex-direction:column; gap:1.5rem; }
.bs-quote-copy{ max-width:62ch; }
.bs-quote-body{ margin-top:.6rem; color:var(--muted); }
.bs-quote-actions{ display:grid; gap:.75rem; }

@media (min-width:760px){
  .bs-quote{ flex-direction:row; align-items:center; justify-content:space-between; gap:2rem; }
  .bs-quote-actions{ flex:0 0 auto; }
}


/* --------------------------------------------------------- cross-linking -- */

.bs-more{ border-top:1px solid var(--line); }
.bs-more-head{
  font-size:var(--eyebrow-size);
  text-transform:uppercase; letter-spacing:var(--tracking-eyebrow);
  color:var(--muted);
  margin-bottom:1rem;
}
.bs-chips{ display:flex; flex-wrap:wrap; gap:.5rem; }
.bs-chip--all{ color:var(--accent-text); border-color:var(--accent); }
.bs-chip--all:hover,
.bs-chip--all:focus-visible{ color:var(--accent-ink); background:var(--accent); border-color:var(--accent); }


/* ---------------------------------------------------------------- reveal -- */
/* MOVED to styles/90-motion.css (2026-07-30).
 *
 * Two things were wrong here, and they were wrong together:
 *
 * 1. DOUBLE FROM-STATE. Every element in boss.html and boss-category.html that
 *    carries class="bs-reveal" ALSO carries data-reveal (likewise bs-stagger /
 *    data-stagger — check the templates, it is all nine of them). So this
 *    file's 16px rise and 53-page-content.css's unscoped 14px rise both hit the
 *    same element and only specificity decided which won. Deleting this block
 *    leaves exactly one from-state, in 90-motion.css. Rise 16px -> 14px.
 *
 * 2. INVERTED GATE. The from-state applied unconditionally and was undone
 *    inside `@media (scripting:none)`. That fails in the wrong direction: a
 *    browser that does not implement the `scripting` media feature drops the
 *    un-hide, so JS-off on such a browser meant a BLANK BOSS PAGE. The
 *    replacement fences the from-state behind `@media (scripting:enabled)`,
 *    where an unknown feature drops the HIDING instead.
 *
 * Do not re-add a from-state for .bs-reveal here. */

@media (prefers-reduced-motion:reduce){
  .bs-cat-grid .cat-card::after{ transition:none; }
  .bs-cat-grid .cat-card:hover::after{ transform:none; }
}

/* ── styles/52-page-rentals.css ── */
/* ============================================================================
   52-page-rentals.css — the /rentals pillar page

   OWNED BY: templates/pages/rentals.html only. Everything is scoped under
   .rentals-page so nothing here can leak onto Equipment or BOSS.

   BREAKPOINTS: 760px and 1024px. There is no third one.

   COLOUR: <body class="theme-rentals"> re-points --accent to safety yellow.
   Nothing below names a colour — it is all var(--accent) / var(--surface) /
   var(--line). The same markup would come out Bobcat orange under
   theme-equipment, which is the point of the token system.
   ========================================================================== */


/* ---------------------------------------------------------------- reveal --
   MOVED to styles/90-motion.css (2026-07-30).

   The block that used to sit here was byte-identical to the one in
   53-page-content.css — 14px rise, .55s ease, the same (scripting:enabled)
   guard — so on this page two equal-specificity rules were racing to produce
   the same result. The site now has exactly ONE [data-reveal] from-state, in
   90-motion.css, which sorts last and therefore also owns the
   prefers-reduced-motion override for it.

   The safety reasoning has not changed and is written out in full there: with
   JS off nothing ever adds .in, so an ungated hidden from-state is a blank
   page. Do not re-add one here. */


/* ------------------------------------------------------------------ hero --
   Split hero: copy on one side, the portrait boom-lift shot on the other.

   The photo is 1200x1800 — aspect-ratio 2/3. The media box is pinned to that
   EXACT ratio at every width, so object-fit:cover has nothing to crop and the
   boom keeps running diagonally out of frame. Do not "tidy" this into a 16/9
   band; the reach of the machine is the whole content of the picture.

   Copy is first in the DOM. On a phone that means h1 → lede → buttons before
   a ~560px-tall image, instead of after it. */

.rentals-page .rn-hero{
  padding-block:clamp(1.75rem,4vw,3.25rem) clamp(2.5rem,6vw,4.5rem);
  background:linear-gradient(180deg,var(--bg-2) 0%,var(--bg) 100%);
  border-bottom:1px solid var(--line);
}

.rn-hero-inner{
  display:grid;
  grid-template-columns:1fr;
  gap:clamp(1.75rem,4vw,2.5rem);
}

.rn-hero-copy .lede{ margin-top:1.1rem; }

.rn-hero-media{
  position:relative;
  aspect-ratio:2/3;
  overflow:hidden;
  background:var(--bg-2);
  border:var(--border);
  border-radius:var(--card-radius);
  box-shadow:var(--shadow);
}
.rn-hero-media img{
  width:100%; height:100%;
  object-fit:cover;
  object-position:center;
}

/* Safety-yellow chip naming the machine. The accent is literally the colour of
   the lift in the photo, so the chip reads as part of the equipment. */
.rn-hero-tag{
  position:absolute; left:0; bottom:0;
  padding:.55rem .9rem;
  background:var(--accent);
  color:var(--accent-ink);
  font-family:var(--font-display);
  font-size:var(--body-xs); font-weight:600;
  text-transform:uppercase; letter-spacing:.09em;
}

.rn-hero-stats{
  display:grid;
  grid-template-columns:repeat(3,minmax(0,1fr));
  gap:.75rem;
  margin-top:2rem;
  padding-top:1.35rem;
  border-top:1px solid var(--line);
}
.rn-stat-n{
  display:block;
  font-family:var(--font-display);
  font-size:1.9rem; line-height:1;
  color:var(--accent);
  font-variant-numeric:tabular-nums;
}
.rn-stat-l{
  display:block;
  margin-top:.3rem;
  font-size:var(--body-xs);
  text-transform:uppercase; letter-spacing:.09em;
  color:var(--muted);
}

@media (min-width:760px){
  .rn-hero-inner{
    grid-template-columns:1fr 1fr;
    align-items:center;
    gap:clamp(1.75rem,4vw,3rem);
  }
}
@media (min-width:1024px){
  .rn-hero-inner{ grid-template-columns:1.15fr .85fr; }
}


/* ----------------------------------------------------------------- terms --
   The use tax and the cancellation rule are commercial terms. They sit
   directly under the hero and the tax panel is a SOLID accent fill, so it is
   the loudest thing on the page after the h1. That is deliberate: a customer
   should never meet the 5.5% for the first time at the counter. */

.rn-term{ display:flex; flex-direction:column; }
.rn-term h3{ font-size:var(--heading-3); }
.rn-term p{ margin-top:.5rem; color:var(--muted); font-size:var(--body-sm); }

.rn-term-figure{
  font-family:var(--font-display);
  font-size:2.4rem; line-height:1;
  letter-spacing:var(--tracking-display);
  margin-bottom:.5rem;
}
.rn-term-figure--alt{ color:var(--accent); }

/* Solid accent panel — every child has to invert off var(--accent-ink), the
   same way .cta-band does it in 20-layout.css. */
.rn-term--tax{
  background:var(--accent);
  border-color:var(--accent);
  color:var(--accent-ink);
}
.rn-term--tax h3,
.rn-term--tax .rn-term-figure{ color:var(--accent-ink); }
.rn-term--tax p{ color:var(--accent-ink); opacity:.82; }

.rn-inline-link{
  color:var(--accent);
  border-bottom:1px solid currentColor;
}
.rn-inline-link:hover,
.rn-inline-link:focus-visible{ color:var(--accent-bright); }


/* --------------------------------------------------------------- periods --
   THE reason this page exists. Day / Weekend / Week / Month were only ever
   pixels inside a scanned rate sheet; here they are real text, one card each,
   hour count large. */

.rn-periods{
  display:grid;
  grid-template-columns:1fr;
  gap:var(--gap);
}
.rn-period{
  background:var(--surface);
  border:var(--border);
  border-top:3px solid var(--accent);
  border-radius:var(--card-radius);
  padding:1.15rem;
}
.rn-period dt{
  font-family:var(--font-display);
  font-size:var(--body-sm); font-weight:600;
  text-transform:uppercase; letter-spacing:.12em;
  color:var(--muted);
}
.rn-period-hours{
  margin-top:.25rem;
  font-family:var(--font-display);
  font-size:2.4rem; line-height:1;
  color:var(--accent);
  font-variant-numeric:tabular-nums;
}
.rn-period-hours span{
  display:block;
  margin-top:.3rem;
  font-family:var(--font-body);
  font-size:var(--body-xs);
  text-transform:uppercase; letter-spacing:.1em;
  color:var(--muted);
}
.rn-period-detail{
  margin-top:.75rem;
  padding-top:.75rem;
  border-top:1px solid var(--line);
  font-size:var(--body-sm);
  color:var(--text);
}

.rn-fineprint{
  margin-top:clamp(1.25rem,2.5vw,1.75rem);
  max-width:80ch;
  font-size:var(--body-sm);
  color:var(--muted);
}

@media (min-width:760px){
  .rn-periods{ grid-template-columns:repeat(2,minmax(0,1fr)); }
}
@media (min-width:1024px){
  .rn-periods{ grid-template-columns:repeat(4,minmax(0,1fr)); }
}


/* ------------------------------------------------------------------ yard -- */

.rn-photo{
  background:var(--bg-2);
  border:var(--border);
  border-radius:var(--card-radius);
  overflow:hidden;
}
.rn-photo img{
  width:100%;
  aspect-ratio:4/3;
  object-fit:cover;
}
.rn-photo figcaption{
  padding:.65rem .85rem;
  border-top:1px solid var(--line);
  font-family:var(--font-display);
  font-size:var(--body-xs); font-weight:600;
  text-transform:uppercase; letter-spacing:.09em;
  color:var(--muted);
}


/* --------------------------------------------------------------- jump nav --
   A REAL anchor list, not a placeholder. It is the no-JS baseline and the
   mobile navigation for a 55-row page. A later agent turns these same anchors
   into filter buttons — the markup is already the right shape for that
   (one <a> per category, href="#cat-<id>"), so leave the hrefs working. */

/* js/20-filter.js rewrites each <a href="#cat-x"> in here into a <button> once
   it has loaded — an anchor works with no JS, a filter button does not, so the
   anchor is what ships and the button is the upgrade. The selectors below name
   BOTH element types for exactly that reason: an `a`-only selector would leave
   the whole nav unstyled the moment the enhancement lands. Everything else
   about the design is untouched. */
.rn-jump{ margin-bottom:clamp(1.5rem,3vw,2.25rem); }
.rn-jump ul{ display:flex; flex-wrap:wrap; gap:.5rem; }
.rn-jump a,
.rn-jump button{
  display:inline-flex; align-items:center; gap:.5rem;
  padding:.5rem .85rem;
  min-height:44px;
  background:var(--surface);
  border:1px solid var(--line-strong);
  border-radius:var(--btn-radius);
  font-family:var(--font-display);
  font-size:var(--body-xs); font-weight:600;
  text-transform:uppercase; letter-spacing:.08em;
  transition:background var(--t),color var(--t),border-color var(--t);
}
.rn-jump a:hover,
.rn-jump a:focus-visible,
.rn-jump button:hover,
.rn-jump button:focus-visible,
.rn-jump button[aria-pressed="true"]{
  background:var(--accent);
  border-color:var(--accent);
  color:var(--accent-ink);
}
.rn-jump-n{
  font-family:var(--font-body);
  font-size:var(--body-xs); font-weight:400;
  letter-spacing:0;
  color:var(--muted);
  font-variant-numeric:tabular-nums;
}
.rn-jump a:hover .rn-jump-n,
.rn-jump a:focus-visible .rn-jump-n,
.rn-jump button:hover .rn-jump-n,
.rn-jump button:focus-visible .rn-jump-n,
.rn-jump button[aria-pressed="true"] .rn-jump-n{ color:var(--accent-ink); opacity:.75; }


/* ----------------------------------------------------------- fleet blocks --
   These are <section>s, so the global `section{padding-block:var(--section-y)}`
   in 20-layout.css applies and has to be cleared — 12 categories at 6rem of
   padding each would add three screens of dead air.

   scroll-margin-top tops up the global html{scroll-padding-top} in 10-reset.css
   (nav-h + 1rem) so a jumped-to category clears the sticky header with a small
   gap instead of sitting flush against it. Keep it small — the two ADD. */

.rn-cat{
  padding-block:0;
  margin-top:clamp(2rem,4vw,3rem);
  scroll-margin-top:.5rem;
}
.rn-cat:first-of-type{ margin-top:0; }

.rn-cat-head{
  display:flex; flex-wrap:wrap; align-items:baseline;
  gap:.35rem .9rem;
  padding-left:.85rem;
  border-left:3px solid var(--accent);
}
.rn-cat-head h3{
  font-size:var(--heading-3);
  text-transform:uppercase;
}
.rn-cat-count{
  font-size:var(--body-xs);
  text-transform:uppercase; letter-spacing:.1em;
  color:var(--muted);
}
.rn-cat-back{
  margin-left:auto;
  font-size:var(--body-xs);
  text-transform:uppercase; letter-spacing:.09em;
  color:var(--muted);
  white-space:nowrap;
}
.rn-cat-back:hover{ color:var(--accent); }

.rn-cat-blurb{
  margin:.7rem 0 1rem;
  padding-left:calc(.85rem + 3px);
  max-width:78ch;
  font-size:var(--body-sm);
  color:var(--muted);
}


/* ----------------------------------------------------------- rate sheets -- */

.rn-sheets-panel .btn-pair{ margin-top:1.35rem; }


/* ------------------------------------------------------------- book band -- */

.rn-book{
  display:grid;
  grid-template-columns:1fr;
  gap:clamp(1.75rem,4vw,3rem);
  align-items:center;
}
.rn-book-copy .lede{ margin-top:1rem; }
.rn-book-media{
  overflow:hidden;
  border:var(--border);
  border-radius:var(--card-radius);
  background:var(--bg-2);
}
.rn-book-media img{
  width:100%;
  aspect-ratio:4/3;
  object-fit:cover;
}
.rn-related{
  margin-top:1.5rem;
  font-size:var(--body-sm);
  color:var(--muted);
}
.rn-related a{
  color:var(--text);
  border-bottom:1px solid var(--line-strong);
}
.rn-related a:hover{ color:var(--accent); border-bottom-color:var(--accent); }

@media (min-width:760px){
  .rn-book{ grid-template-columns:1fr 1fr; }
  .rn-book-media img{ aspect-ratio:5/4; }
}

/* ── styles/53-page-content.css ── */
/* ============================================================================
   53-page-content.css — /, /about, /service, /parts

   BREAKPOINTS: 760px and 1024px. There is no third one.

   NAMING: everything page-specific is prefixed `pc-`. This fragment is
   concatenated LAST, so an unprefixed `.hero` or `.figure` here would outrank
   .eq-hero / .rn-hero / .bs-hero in 50-52 and silently restyle three other
   agents' pages. The only unprefixed selectors below are deliberate additions
   to SHARED components (.panel--top, .badge-row--lg, .on-light .badge,
   .btn-pair--center) — each one additive, none redefining an existing rule.

   COLOUR: nothing here names a colour. Every accent is var(--accent), which
   the theme-* class on <body> — or on any ancestor element — re-points. The
   homepage depends on that: <body> has no theme, and each pillar teaser (plus
   the BOSS band and the /parts panels) carries its own theme-* wrapper.

   TYPE: sizes come from the tokens only (--heading-3, --body-sm, --body-xs).
   No literal font-size anywhere in this file.
   ========================================================================== */


/* ----------------------------------------------------------- reveal state --
   MOVED to styles/90-motion.css (2026-07-30).

   This block was unscoped (`.anim [data-reveal]`), so despite living in a file
   named for four content pages it was the from-state for the WHOLE site — and
   it collided with three other copies: an identical one in 52-page-rentals.css,
   a bespoke one in 50-page-equipment.css, and a `.bs-reveal` one in
   51-page-boss.css that landed on the same elements as this one on every BOSS
   page. 90-motion.css now owns a single global from-state and explicitly
   excludes .theme-equipment, which keeps its own.

   The safety reasoning is unchanged and written out in full there: the gate on
   `scripting: enabled` is what stops <html class="anim"> turning into a blank
   page when JavaScript never runs. Do not re-add a from-state here.
-------------------------------------------------------------------------- */


/* --------------------------------------------------------------- pc-split --
   The one two-column primitive these four pages use: hero, about intro, visit
   block, parts pair. Single column until 1024 — at 760 a photo beside a
   paragraph leaves both halves too narrow.
-------------------------------------------------------------------------- */

/* minmax(0,1fr), not 1fr — a bare 1fr track has an auto minimum, so one long
   unbreakable string inside it pushes the grid wider than 375px instead of
   wrapping. Same reason .grid in 30-components.css spells it out. */
.pc-split{
  display:grid;
  gap:var(--gap);
  grid-template-columns:minmax(0,1fr);
}
@media (min-width:1024px){
  .pc-split{
    grid-template-columns:minmax(0,1fr) minmax(0,1fr);
    column-gap:clamp(2rem,4vw,4rem);
    align-items:center;
  }
  .pc-split--wide-first{ grid-template-columns:minmax(0,1.1fr) minmax(0,.9fr); }
  .pc-split--top{ align-items:start; }
}


/* -------------------------------------------------------------- pc-figure --
   Shared by the hero photo and the /about photo. The caption carries a short
   accent rule, which ties the photo into the page's colour system without
   tinting the photo itself.
-------------------------------------------------------------------------- */

.pc-figure{ margin:0; }

.pc-figure img{
  width:100%;
  border:var(--border);
  border-radius:var(--card-radius);
}

.pc-figure figcaption{
  margin-top:.9rem;
  padding-left:.8rem;
  border-left:2px solid var(--accent);
  font-size:var(--body-sm);
  color:var(--muted);
}
.on-light .pc-figure figcaption{ color:var(--muted-light); }


/* ---------------------------------------------------------------- pc-hero --
   Homepage only. The markup also carries the bare `hero` class, which is an
   anim.js hook, not a style hook: anything inside .hero is revealed
   immediately instead of waiting for the IntersectionObserver.

   Copy comes first in the DOM so a 375px screen reads the headline before it
   scrolls past a square photo.
-------------------------------------------------------------------------- */

.pc-hero{ padding-block:clamp(2.25rem,5vw,4rem) var(--section-y); }

.pc-hero h1{ margin-top:.15rem; }
.pc-hero .lede{ margin-top:1.1rem; }
.pc-hero .specs{ margin-top:1.6rem; }

@media (min-width:1024px){
  /* The source photo is square (1800x1773). Left unbounded it makes the hero
     taller than the viewport on a laptop, so it is capped and cropped from the
     centre — the sign and the two people both sit inside that crop. */
  .pc-hero .pc-figure img{ max-height:34rem; object-fit:cover; }
}


/* ----------------------------------------------------------- pc-page-head --
   Inner pages: breadcrumbs, then the section_head that carries the h1.
-------------------------------------------------------------------------- */

.pc-page-head{ padding-bottom:clamp(1.5rem,3vw,2.5rem); }
.pc-page-head .btn-pair{ margin-top:1.75rem; }


/* ------------------------------------------------------------ pc-spectrum --
   THE HOMEPAGE'S ONE PIECE OF PURPOSE-BUILT COLOUR.

   Three segments, generated from site.pillars in the same order as the three
   teaser cards below it, each wrapped in its own theme-* class so it paints
   var(--accent). It reads as a legend: it states that orange, red and yellow
   are ONE SET OF THREE before the eye meets them scattered across three
   cards. From 1024 it uses the same track sizing and gap as .grid--3, so each
   segment sits directly above the card whose 3px top border it matches.

   Decorative only — aria-hidden in the markup, no meaning carried by colour.
-------------------------------------------------------------------------- */

.pc-spectrum{
  display:grid;
  grid-template-columns:repeat(3,minmax(0,1fr));
  gap:.3rem;
  height:4px;
  margin-bottom:1.4rem;
}
.pc-spectrum > span{ display:block; background:var(--accent); }

@media (min-width:1024px){
  .pc-spectrum{ gap:var(--gap); }
}


/* ------------------------------------------------------------ pc-band-news --
   Full-bleed band for the BOSS announcement on / and /service. Carries
   theme-boss in the markup, so everything inside is BOSS red on an otherwise
   neutral page.
-------------------------------------------------------------------------- */

.pc-band-news{
  background:var(--bg-2);
  border-top:1px solid var(--line);
  border-bottom:1px solid var(--line);
}
.pc-band-news .section-head{ margin-bottom:1.25rem; }


/* ------------------------------------------------------------- panel--top --
   SHARED COMPONENT ADDITION. Accent rule along the top edge, matching
   .pillar-card. Used where two differently-themed panels sit side by side
   (/parts) and the accent has to stay in one strictly repeated slot.
-------------------------------------------------------------------------- */

.panel--top{ border-top:3px solid var(--accent); }


/* --------------------------------------------------------------- pc-panel -- */

.pc-panel-title{
  font-size:var(--heading-3);
  text-transform:uppercase;
}
.pc-panel-body{ margin-top:.8rem; color:var(--muted); }
.on-light .pc-panel-body{ color:var(--muted-light); }
.pc-panel-stack{ display:grid; gap:var(--gap); }
.panel .btn-pair{ margin-top:1.4rem; }

/* Manufacturer logo lock-up. logo-bobcat.png has a solid orange background —
   it can only sit on a dark surface, never inside .on-light. */
.pc-panel-logo{
  width:auto; height:3.4rem;
  margin-block:1.1rem;
  border-radius:var(--card-radius);
}

/* nap.html inside a panel — the hours table sets its own max-width. */
.pc-nap-panel .nap{ margin-bottom:1.25rem; }


/* ---------------------------------------------------------- pc-brand-tile --
   TYPOGRAPHIC ON PURPOSE. Of the four brands sold, logo-bobcat.png and
   logo-generac.png ship with solid (non-transparent) backgrounds and BOSS has
   no licensed asset at all. A logo row would be two coloured rectangles, one
   transparent mark and one hole — four different objects, not a system.
   Setting all four in the display face gives BOSS equal standing and invents
   nothing.
-------------------------------------------------------------------------- */

.pc-brand-tile{
  border:var(--border);
  border-radius:var(--card-radius);
  transition:border-color var(--t);
}
.pc-brand-tile a{
  display:flex; align-items:center; justify-content:space-between;
  gap:.75rem;
  height:100%;
  padding:1.15rem;
  min-height:5rem;
}
.pc-brand-name{
  font-family:var(--font-display);
  font-size:var(--heading-3);
  font-weight:600;
  text-transform:uppercase;
  letter-spacing:.04em;
  line-height:1.15;
}

.on-light .pc-brand-tile{ border-color:var(--line-light); }
.on-light .pc-brand-tile:hover{ border-color:var(--accent-deep); }
.on-light .pc-brand-tile a:hover{ color:var(--accent-deep); }


/* ----------------------------------------------------------------- badges --
   SHARED COMPONENT ADDITION. .badge ships in 30-components.css coloured for a
   dark surface; on a light band its var(--muted) grey falls to roughly 2.4:1
   against --bg-light, so ink and border both need re-pointing. Same
   component, correct colours — no .chip variant invented.
-------------------------------------------------------------------------- */

.on-light .badge{ color:var(--muted-light); border-color:var(--line-light); }
.on-light .badge-accent{ color:var(--accent-deep); border-color:var(--accent-deep); }

/* Roomier badges for a standalone list of brand or place names. */
.badge-row--lg{ gap:.55rem; margin-top:1.25rem; }
.badge-row--lg .badge{ padding:.45rem .8rem; font-size:var(--body-sm); }


/* --------------------------------------------------------------- pc-prose --
   Running body copy in the client's own words. Measure held by .wrap-narrow
   on the container.
-------------------------------------------------------------------------- */

.pc-prose > * + *{ margin-top:1.15rem; }
.pc-prose p{ color:var(--muted); }
.on-light .pc-prose p{ color:var(--muted-light); }


/* ------------------------------------------------------------ small parts -- */

.pc-note{
  margin-top:1.25rem;
  font-size:var(--body-sm);
  color:var(--muted);
}
.on-light .pc-note{ color:var(--muted-light); }
.pc-note--center{ text-align:center; }
.pc-note a{ text-decoration:underline; text-underline-offset:.2em; }

/* SHARED COMPONENT ADDITION. .btn-pair stacks full-width below 760 on purpose
   (a long label must not overflow at 375px) and only becomes a flex row above
   it, so centring is a 760-and-up concern. */
@media (min-width:760px){
  .btn-pair--center{ justify-content:center; }
}

/* ── styles/54-testimonials.css ── */
/* ============================================================================
   Testimonials — see templates/partials/testimonials.html

   Deliberately quiet. These are short, real quotes from a small-town dealer's
   Facebook page, not marketing copy. Oversized pull-quote treatment would make
   three genuine sentences look like invented filler, which is the opposite of
   what they are for. Restraint reads as authentic.
   ========================================================================== */

.testimonials{ background:var(--bg-2); }

.tm-grid{
  display:grid;
  gap:var(--gap);
  grid-template-columns:1fr;
  margin-top:2rem;
}
@media (min-width:760px){ .tm-grid{ grid-template-columns:repeat(2,minmax(0,1fr)); } }
@media (min-width:1024px){ .tm-grid{ grid-template-columns:repeat(3,minmax(0,1fr)); } }

/* One review on its own shouldn't stretch the full width — it reads as a
   layout accident. Cap it and centre it instead. */
.tm-grid:has(> :only-child){ grid-template-columns:minmax(0,42rem); justify-content:center; }

.tm-card{
  display:flex;
  background:var(--surface);
  border:var(--border);
  border-top:2px solid var(--accent);
  border-radius:var(--card-radius);
  padding:clamp(1.25rem,3vw,1.75rem);
}
.tm-card > figure{ display:flex; flex-direction:column; gap:1rem; height:100%; }

.tm-quote p{
  font-size:1.05rem;
  line-height:1.55;
  color:var(--text);
}
/* The opening mark is decorative, so it must not reach the accessibility tree
   or the text selection. ::before keeps it out of both. */
.tm-quote p::before{
  content:'\201C';
  font-family:var(--font-display);
  font-size:2.5rem;
  line-height:0;
  color:var(--accent-text);
  margin-right:.15em;
  vertical-align:-.35em;
}

.tm-by{
  margin-top:auto;
  display:flex;
  flex-direction:column;
  gap:.15rem;
  font-size:var(--body-sm);
}
.tm-name{
  font-family:var(--font-display);
  font-weight:600;
  letter-spacing:.02em;
  color:var(--heading-color);
  text-transform:uppercase;
}
.tm-signoff{ color:var(--muted); }
.tm-src{
  color:var(--muted);
  font-size:var(--body-xs);
  text-transform:uppercase;
  letter-spacing:.08em;
}
.tm-date{ opacity:.8; }

.on-light .testimonials,
.testimonials.on-light{ background:var(--bg-light); }
.on-light .tm-card{ background:#fff; border-color:var(--line-light); }
.on-light .tm-quote p{ color:var(--on-light); }
.on-light .tm-quote p::before{ color:var(--accent-deep); }
.on-light .tm-name{ color:var(--on-light); }
.on-light .tm-signoff,
.on-light .tm-src{ color:var(--muted-light); }

/* ── styles/90-motion.css ── */
/* ============================================================================
   90-motion.css — the site's motion contract

   SORTS LAST ON PURPOSE. build.py concatenates styles/*.css in filename order,
   so `90-` lands after every page fragment. That is load-bearing: the
   reduced-motion block at the bottom of this file has to be able to overrule a
   from-state written in any fragment, including one written next year.

   ── THE CRITICAL GUARDRAIL ─────────────────────────────────────────────────
   base.html hard-codes <html class="anim">. Nothing removes that class except
   anim.js's own catch block. So a from-state written as

       .anim [data-reveal]{ opacity:0 }

   with no further gate is a BLANK PAGE for anyone whose JavaScript never runs
   — there is nothing left to add `.in`. Every from-state below is therefore
   fenced behind `@media (scripting: enabled)`, and every failure mode lands on
   "visible":

     JS switched off      -> scripting:enabled is false -> block never applies
     browser too old      -> feature unknown, whole block dropped
     anim.js throws       -> it strips html.anim -> selector stops matching
     reduced motion       -> no-preference is false -> block never applies

   Four independent ways to fail, all of them to shown. Never write a hidden
   from-state outside a gate like this one.

   ── CONSOLIDATION (2026-07-30) ─────────────────────────────────────────────
   The same [data-reveal] from-state had been written four times, in four
   fragments, with two different rise values and three different gates. On the
   BOSS pages that produced a genuine double from-state: boss.html puts
   class="bs-reveal" AND data-reveal on the SAME element, so 51's `.bs-reveal`
   rule (16px, ungated) and 53's `[data-reveal]` rule (14px, gated) both hit it
   and only specificity decided the outcome. Worse, 51's gate was inverted — it
   hid by default and un-hid inside `@media (scripting:none)`, so a browser that
   does not know the `scripting` feature AND has JS off got a blank BOSS page.

   Resolved as follows:
     • 52-page-rentals.css   from-state DELETED — identical to this one.
     • 53-page-content.css   from-state DELETED — identical to this one.
     • 51-page-boss.css      .bs-reveal / .bs-stagger from-state DELETED. Every
                             element carrying those classes also carries
                             data-reveal / data-stagger, so this file covers
                             them. Rise goes 16px -> 14px. That deletion is
                             what fixes the inverted gate.
     • 50-page-equipment.css KEPT, and this file's rule deliberately excludes
                             .theme-equipment via :not(). Equipment runs its own
                             rise (--eq-rise) and its own slower duration
                             (--t-slow) as a design choice, it is already
                             correctly gated on (scripting:enabled), and the two
                             selectors are mutually exclusive — so no element on
                             the site can ever receive two different from-states.

   ONE from-state per element. That is the whole rule.
   ========================================================================== */


/* ---------------------------------------------------------- reveal state --
   anim.js observes [data-reveal] and [data-stagger] and adds `.in` — to the
   element itself for a reveal, to each direct CHILD (with an inline
   transition-delay) for a stagger. It writes no styles of its own, so the
   before-and-after lives here.

   Values (14px / .55s ease) are the ones 52 and 53 already shipped, kept
   byte-identical so this consolidation is a no-op on screen.
-------------------------------------------------------------------------- */

@media (scripting:enabled) and (prefers-reduced-motion:no-preference){

  html.anim body:not(.theme-equipment) [data-reveal],
  html.anim body:not(.theme-equipment) [data-stagger] > *{
    opacity:0;
    transform:translateY(14px);
    transition:opacity .55s ease,transform .55s ease;
  }

  html.anim body:not(.theme-equipment) [data-reveal].in,
  html.anim body:not(.theme-equipment) [data-stagger] > .in{
    opacity:1;
    transform:none;
  }

}


/* ============================================================================
   REDUCED MOTION

   `prefers-reduced-motion: reduce` is a user telling the browser that motion
   makes them ill. It is not a style preference. Everything below is therefore
   !important: it has to beat any from-state in any fragment at any
   specificity, including fragments that do not exist yet. This is the one
   place in the codebase where !important is the correct tool — the alternative
   is an accessibility guarantee that a future 60-page-whatever.css can
   silently break.

   The rules are ordered: un-hide first, then stop the motion, then remove the
   ornaments that exist only to move.
   ========================================================================== */

@media (prefers-reduced-motion:reduce){

  /* ---- 1. everything is visible, instantly ------------------------------
     A from-state is only safe while something is guaranteed to un-hide it.
     Under reduce, nothing is: anim.js still adds `.in`, but a user who cannot
     tolerate motion should not be waiting on an IntersectionObserver to be
     allowed to read the page. Both selectors below are listed — the plain one
     AND every scoped variant a page fragment owns — so the guarantee does not
     depend on which fragment won. */
  html.anim [data-reveal],
  html.anim [data-reveal].in,
  html.anim [data-stagger] > *,
  html.anim [data-stagger] > .in,
  html.anim body.theme-equipment [data-reveal],
  html.anim body.theme-equipment [data-stagger] > *,
  html.anim .rentals-page [data-reveal],
  html.anim .rentals-page [data-stagger] > *,
  html.anim .bs-reveal,
  html.anim .bs-stagger > *,
  html.anim .wr,
  html.anim .tw-body{
    opacity:1 !important;
    transform:none !important;
    transition:none !important;
    animation:none !important;
    visibility:visible !important;
  }

  /* ---- 2. nothing moves -------------------------------------------------
     .01ms rather than 0: a zero-length transition never fires transitionend,
     and any script waiting on that event would hang forever. This keeps the
     event, removes the movement.

     animation-iteration-count:1 is what kills an INFINITE animation — a
     duration of .01ms on an infinite loop is still an infinite loop, just a
     fast one, and it will still burn a repaint every frame for as long as the
     page is open. */
  *,*::before,*::after{
    animation-duration:.01ms !important;
    animation-delay:0ms !important;
    animation-iteration-count:1 !important;
    transition-duration:.01ms !important;
    transition-delay:0ms !important;
    scroll-behavior:auto !important;
  }

  /* 10-reset.css sets html{scroll-behavior:smooth}. A smooth scroll is motion
     the user did not ask for, and the universal selector above does not
     reliably reach the scrolling element on every engine. Name it directly. */
  html{ scroll-behavior:auto !important; }

  /* ---- 3. no hover-transform ornaments ---------------------------------- */
  .card:hover,
  .card:hover .card-media img,
  .bs-cat-grid .cat-card:hover::after,
  .eq-next-card:hover,
  .pillar-card:hover{ transform:none !important; }

  /* ---- 4. things that exist only to move --------------------------------
     The typewriter caret is a blinking bar and the scroll-progress bar is a
     width animation tied to scroll position. Neither carries information —
     the caret marks a cursor that is no longer typing (anim.js pastes the
     text in one go under reduce) and the progress bar duplicates the
     scrollbar. Both are removed rather than frozen. */
  .tw-caret,
  .scroll-progress{ display:none !important; }

  /* ---- 5. the filter chrome --------------------------------------------
     Filtering itself is not motion — hiding a row is instant either way — but
     the button and input states are transitioned in 30-components.css. */
  .filter-btn,
  .filter-search-input,
  .filter-clear{ transition:none !important; }

}
