/* ==========================================================================
   Pranthi Enterprise — stylesheet
   Hand-written CSS, custom-property tokens. No framework, no build step.
   ========================================================================== */

:root {
  /* ---- Color: semantic roles, light theme ----
     Tokens are named by ROLE, not by shade, so a rule reads as its intent
     ("surface", "text-muted") and the palette can move without the names
     lying about what they do. */

  /* The ground is white and the greys are cool, on purpose. This used to be a
     warm off-white (#FAF8F3) whose comment claimed it matched the logo's bone —
     but the logo is amber #F2A413 and ink #14171B, and the bone #F5F3ED appears
     in it twice. Nothing required the cream, and warm off-white plus a
     condensed display face is the house style of generated web design. A white
     page with cool hairlines reads as a parts catalogue instead, which is what
     this business actually is. Note the text greys below were ALREADY cool —
     they were sitting on a warm ground, which is part of why it looked off. */
  --c-bg: #FFFFFF;           /* page */
  --c-surface: #FFFFFF;      /* cards, modal, elevated surfaces */
  --c-surface-2: #F1F3F5;    /* section bands, brand strip */
  --c-surface-3: #E4E8EA;    /* tile label plate — one step darker than the
                                band, so it separates from BOTH the white photo
                                above it (ΔL* 8.2) and a --c-surface-2 band
                                behind it (ΔL* 4.0). See the tile notes. */
  --c-border: #DDE1E4;       /* hairlines / dividers */
  --c-border-strong: #C3C9CE;/* input borders, stronger separation */
  --c-text: #14171B;         /* headings and primary text */
  --c-text-muted: #5C6269;   /* body copy — 6.2:1 on white */
  --c-text-subtle: #5F666D;  /* captions, footnotes — 5.2:1 on the
                                --c-surface-2 band */

  /* --c-surface deliberately equals --c-bg. Cards are separated by hairlines
     rather than by a tint, which is the catalogue look; every card-like rule
     (.card, .fallback-cta, .field, .part-cta) already carries a border, and the
     sticky header keeps its border-bottom and --shadow-1. */

  /* Accent is split. #F2A413 on white is ~2.1:1 — fine as a fill or a rule,
     far under 4.5:1 as text. Use --c-accent for fills/borders and
     --c-accent-text wherever amber is TEXT on a light ground (5.4:1 on white).
     The fill matches the logo artwork exactly; it used to be #F0A020, which
     was close to the logo without being it. */
  --c-accent: #F2A413;
  --c-accent-rgb: 242, 164, 19;
  --c-accent-text: #9A5B00;
  --c-accent-hover: #DB9310;

  /* Inverted contexts. Text sitting on a photo scrim or the dark footer must
     never track --c-text, or a palette change turns it invisible. */
  --c-on-dark: #F2F4F5;
  --c-on-dark-muted: rgba(242, 244, 245, 0.72);
  --c-on-dark-subtle: rgba(242, 244, 245, 0.55);
  --c-on-dark-border: rgba(242, 244, 245, 0.16);
  --c-footer-bg: #14171B;    /* dark footer anchors the light page */
  --c-photo-bed: #14171B;    /* .photo load-time background under images */

  --c-whatsapp: #25D366;  /* reserved for WhatsApp actions only */
  --c-danger-500: #C0392B;

  /* Elevation — tinted with the ink token rather than pure black, so shadows
     read as depth rather than as grey smudge on the white page. */
  --shadow-1: 0 1px 2px rgba(20,23,27,0.05), 0 1px 3px rgba(20,23,27,0.06);
  --shadow-2: 0 2px 6px rgba(20,23,27,0.07), 0 6px 16px rgba(20,23,27,0.08);
  --shadow-3: 0 8px 24px rgba(20,23,27,0.10), 0 16px 48px rgba(20,23,27,0.10);
  --shadow-up: 0 -2px 12px rgba(20,23,27,0.10);

  /* Type — one superfamily, not three defaults.
     This was Oswald over Inter, which is the most common pairing in generated
     design and isn't really a pairing at all: two unrelated faces each picked
     as a default. IBM Plex was drawn for an engineering company, the three cuts
     are designed against each other, and Plex Mono was already loaded here — so
     this removes a mismatched family rather than adding one. */
  --f-display: 'IBM Plex Sans Condensed', 'Arial Narrow', sans-serif;
  --f-body: 'IBM Plex Sans', -apple-system, BlinkMacSystemFont, sans-serif;
  --f-mono: 'IBM Plex Mono', ui-monospace, monospace;

  /* Spacing — 4px base */
  --s-1: 4px;  --s-2: 8px;  --s-3: 12px; --s-4: 16px; --s-5: 20px;
  --s-6: 24px; --s-8: 32px; --s-10: 40px; --s-14: 56px; --s-20: 80px;

  /* Radii — nearly square. These were 4/8/12px, "gently softened from the
     original hard-edged look", which softened it toward a consumer app and away
     from the trade-supply register the rest of the site is aiming for. */
  --r-sm: 2px;
  --r-md: 3px;
  --r-lg: 4px;

  /* Borders */
  --bw-1: 1px;
  --bw-2: 2px;
  --bw-3: 3px;

  /* Motion */
  --ease-out: cubic-bezier(0.2, 0, 0, 1);
  --dur-fast: 120ms;
  --dur-press: 80ms;
  --dur-modal: 240ms;
  --dur-slow: 350ms;

  --page-pad: 20px;
  --container-max: 1280px;
}

@media (min-width: 1024px) {
  :root { --page-pad: 40px; }
}

/* ---- Reset ---- */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
  margin: 0;
  background: var(--c-bg);
  color: var(--c-text);
  font: 400 15px/1.55 var(--f-body);
  -webkit-font-smoothing: antialiased;
}
h1, h2, h3, h4, p, figure { margin: 0; }
ul { margin: 0; padding: 0; list-style: none; }
img { max-width: 100%; display: block; }
a { color: inherit; text-decoration: none; }
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }
input, select, textarea { font: inherit; color: inherit; }

main:focus { outline: none; }

/* Site-wide focus token */
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
[tabindex]:focus-visible {
  outline: var(--bw-2) solid var(--c-accent);
  outline-offset: 3px;
}

.container {
  max-width: var(--container-max);
  margin: 0 auto;
  padding-left: var(--page-pad);
  padding-right: var(--page-pad);
}

.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0; padding: 0; margin: -1px;
}

.skip-link {
  position: absolute;
  top: -48px; left: var(--s-4);
  background: var(--c-accent);
  color: #14171B;
  font: 700 13px/1 var(--f-body);
  padding: var(--s-3) var(--s-4);
  z-index: 200;
  transition: top var(--dur-fast) var(--ease-out);
}
.skip-link:focus { top: var(--s-4); }

/* ---- Type roles ---- */

/* Micro-labels follow one rule: MONOSPACE IS FOR DATA. A part code, a machine
   code, a count, a step number — things that are looked up or compared. Anything
   that is just a label for a section of a page uses the body font.
   This used to be inverted: every .eyebrow was 10px mono, uppercase, 2.5px
   letterspaced, 100 times across the site. Mono applied to ordinary prose labels
   is a large part of what made the page feel machine-written, and at 10px it was
   hard to read as well. The default below is therefore the section label, and
   .eyebrow--code opts back in to mono where the content really is data. */
.eyebrow {
  font: 600 12px/1.2 var(--f-body);
  letter-spacing: 0.01em;
  text-transform: none;
  color: var(--c-text-subtle);
}
.eyebrow--code {
  font: 500 11px/1 var(--f-mono);
  letter-spacing: 1.5px;
  text-transform: uppercase;
  color: var(--c-accent-text);
}
.eyebrow--muted { color: var(--c-text-subtle); }
/* On a photo scrim or the dark footer the bright accent is the legible one. */
.hero-inner .eyebrow,
.page-hero--photo .eyebrow,
.site-footer .eyebrow { color: var(--c-accent); }

.h-display { font: 600 42px/0.95 var(--f-display); letter-spacing: 0.005em; text-transform: uppercase; color: var(--c-text); }
.h1 { font: 600 30px/0.98 var(--f-display); letter-spacing: 0.01em; text-transform: uppercase; color: var(--c-text); }
.h2 { font: 600 22px/1.05 var(--f-display); letter-spacing: 0.02em; text-transform: uppercase; color: var(--c-text); }
/* .h3 is sentence case while the larger roles stay uppercase. Uppercase at
   display sizes reads as trade signage; at 17-22px on a card title it just
   reads as a template, and it costs legibility on the densest pages. */
.h3 { font: 600 17px/1.15 var(--f-display); letter-spacing: 0.01em; color: var(--c-text); }
.body-lg { font: 400 15px/1.5 var(--f-body); color: var(--c-text-muted); }
.body { font: 400 13px/1.55 var(--f-body); color: var(--c-text-muted); }
.small { font: 400 12px/1.5 var(--f-body); color: var(--c-text-subtle); }

@media (min-width: 1024px) {
  .h-display { font-size: 88px; line-height: 0.9; }
  .h1 { font-size: 56px; line-height: 0.95; }
  .h2 { font-size: 44px; }
  .h3 { font-size: 22px; }
  .body-lg { font-size: 18px; }
  .body { font-size: 14px; }
}

/* ---- Buttons ---- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-2);
  min-height: 48px;
  padding: 0 var(--s-6);
  font: 700 13px/1 var(--f-body);
  letter-spacing: 0.02em;
  border: var(--bw-1) solid transparent;
  white-space: nowrap;
}
/* Filled buttons keep dark ink on their bright fill — that pairing is what
   passes contrast, so it must not follow the page background token. */
.btn-whatsapp { background: var(--c-whatsapp); color: #0F1113; box-shadow: var(--shadow-1); }
.btn-whatsapp:hover { background: #21bd5c; color: #0F1113; }
.btn-outline { background: transparent; border-color: var(--c-accent); color: var(--c-accent-text); }
.btn-outline:hover { background: rgba(var(--c-accent-rgb), 0.10); color: var(--c-accent-text); }
.btn-solid { background: var(--c-accent); color: #14171B; box-shadow: var(--shadow-1); }
.btn-solid:hover { background: var(--c-accent-hover); color: #14171B; }
.btn-ghost { background: transparent; border-color: var(--c-border-strong); color: var(--c-text); }
.btn-ghost:hover { border-color: var(--c-accent); color: var(--c-accent-text); }
.btn-block { width: 100%; }
.btn svg { flex-shrink: 0; }
.btn { border-radius: var(--r-sm); transition: background var(--dur-fast) var(--ease-out), border-color var(--dur-fast) var(--ease-out), box-shadow var(--dur-fast) var(--ease-out), transform var(--dur-fast) var(--ease-out); }

/* A pressed state. There was only :hover, which does not exist on a phone —
   and this audience is on a phone at a jobsite. Tapping a button that hands off
   to WhatsApp gave no feedback at all until the OS switched apps, which reads
   as a dead tap and invites a second one. */
.btn:active { transform: translateY(1px); }
.btn-whatsapp:active { background: #1da851; box-shadow: none; }
.btn-solid:active { background: var(--c-accent-hover); box-shadow: none; }
.btn-outline:active { background: rgba(var(--c-accent-rgb), 0.18); }
.btn-ghost:active { background: var(--c-surface-2); }
@media (prefers-reduced-motion: reduce) {
  .btn { transition: background var(--dur-fast) var(--ease-out), border-color var(--dur-fast) var(--ease-out); }
  .btn:active { transform: none; }
}

/* The WhatsApp mark reads best a little larger than the label it sits beside,
   and it is optically lighter than the text because it is an outline. The size
   lives here rather than on 25 inline width/height pairs. */
.btn-whatsapp svg { width: 17px; height: 17px; }
.btn-header svg { width: 16px; height: 16px; }
@media (prefers-reduced-motion: reduce) { .btn { transition: none; } }

/* ---- Header / nav ---- */
.site-header {
  position: sticky;
  top: 0;
  z-index: 90;
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: 56px;
  padding: 0 var(--page-pad);
  background: var(--c-surface);
  border-bottom: var(--bw-1) solid var(--c-border);
  box-shadow: var(--shadow-1);
}
@media (min-width: 1024px) { .site-header { height: 74px; } }

.brand { display: flex; align-items: center; gap: var(--s-3); }
/* Header uses the logo artwork AS SUPPLIED (light grounds); the dark
   recolours are footer-only now. See images/CREDITS.md.
   The tagline only survives above ~50px, so the header shows the no-tagline
   lockup on mobile and swaps to the full one with the tagline on desktop,
   where the 74px header gives it room. */
.brand picture { display: flex; }
.brand-logo {
  height: 42px;
  width: auto;
  display: block;
  flex-shrink: 0;
}
@media (min-width: 1024px) { .brand-logo { height: 66px; } }
/* Footer has vertical room, so the tagline runs at a properly legible size. */
.brand-logo-full {
  height: 76px;
  width: auto;
  max-width: 100%;
  display: block;
}
@media (min-width: 1024px) { .brand-logo-full { height: 92px; } }
.brand-word {
  font: 600 15px/1 var(--f-display);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--c-text);
}
@media (min-width: 1024px) { .brand-word { font-size: 18px; } }
.brand-sub {
  font: 500 11px/1 var(--f-body);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--c-text-subtle);
  margin-top: 4px;
  display: none;
}
@media (min-width: 1024px) { .brand-sub { display: block; } }

.header-actions { display: flex; align-items: center; gap: var(--s-4); }
/* Below 1024px the brand and the actions row together run a couple of pixels
   past the content box, which put a hairline horizontal scroll on every page.
   Tightening the gap and letting the brand shrink clears it. */
@media (max-width: 1023px) {
  .header-actions { gap: var(--s-3); }
  .brand { min-width: 0; }
  .brand-word { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
}
/* At 320px the row lands within ~3px of the content box, which is close enough
   that a font or rendering difference would push the menu button off the edge
   again. One step tighter on the smallest phones buys back 12px. */
@media (max-width: 379px) {
  .header-actions { gap: var(--s-2); }
}
.icon-btn {
  width: 40px; height: 40px;
  display: grid; place-items: center;
  color: var(--c-text);
}
/* flex-shrink:0 is load-bearing. The brand logo also refuses to shrink and the
   WhatsApp button is nowrap, so any overflow in this row lands on the last
   child — which is this button. Below ~375px the row ran 6-61px over and the
   menu button was pushed off-screen entirely, leaving no way to reach the nav
   on a phone. Whatever else gets squeezed, this must not. */
.nav-toggle {
  display: flex;
  flex-direction: column;
  /* Both of these are load-bearing, and neither is optional.
     .icon-btn above sets place-items:center, which this rule does NOT reset —
     in a column flex container that resolves to align-items:center, so the
     bars size to their content instead of stretching. The spans are empty, so
     that made them 0px wide: three 2px-tall bars of nothing. The bars now carry
     an explicit width instead of relying on stretch, and justify-content
     centres them in the 40px box rather than stacking them against the top. */
  align-items: center;
  justify-content: center;
  gap: 5px;
  /* No width here on purpose: the button keeps .icon-btn's 40x40 so the tap
     target stays finger-sized, and only the bars are 22px. It used to set
     width:22px, which shrank the whole target. */
  flex-shrink: 0;
}
.nav-toggle span { width: 22px; height: 2px; background: var(--c-text); transition: transform var(--dur-fast) var(--ease-out), opacity var(--dur-fast); }
.nav-toggle[aria-expanded="true"] span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
.nav-toggle[aria-expanded="true"] span:nth-child(2) { opacity: 0; }
.nav-toggle[aria-expanded="true"] span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }

.primary-nav { display: none; }
@media (min-width: 1024px) {
  .primary-nav {
    display: flex;
    align-items: center;
    gap: var(--s-8);
    font: 500 14px/1 var(--f-body);
    color: var(--c-text-muted);
  }
  /* The current page is marked by a RULE as well as a colour. It was colour
     alone — --c-text-muted (L* 41.3) against --c-accent-text (L* 44.7), a
     lightness difference of 3.5, so the two separate almost entirely by hue and
     a greyscale or colour-blind user saw no active item at all. WCAG 1.4.1.
     The nav items are position:relative for this; .nav-item already is. */
  .primary-nav a { transition: color var(--dur-fast) var(--ease-out); }
  .primary-nav a[aria-current="page"] { color: var(--c-accent-text); }
  /* Scoped to direct children so the rule marks the top-level item and never a
     link inside the Products dropdown. No :has() needed — the build puts
     aria-current on the <a> itself (applyNavCurrent in tools/lib/partials.mjs),
     so the marker hangs off the element that already carries the state. */
  .primary-nav > a[aria-current="page"],
  .primary-nav .nav-item > a[aria-current="page"] { position: relative; }
  .primary-nav > a[aria-current="page"]::after,
  .primary-nav .nav-item > a[aria-current="page"]::after {
    content: "";
    position: absolute;
    left: 0; right: 0;
    bottom: -6px;
    height: var(--bw-2);
    background: var(--c-accent);
  }
  .primary-nav a:hover { color: var(--c-text); }
  .header-phone { font: 500 13px/1 var(--f-body); color: var(--c-text-muted); white-space: nowrap; }
  .header-phone:hover { color: var(--c-accent-text); }
}
/* Header WhatsApp button. Sized here rather than inline so the label can be
   dropped responsively — it is ~67px of a 341px row that only has ~280px to
   play with on a 320px phone. The label is visually hidden rather than
   display:none so the link keeps its accessible name for screen readers. */
.btn-header { height: 40px; min-height: 40px; padding: 0 var(--s-4); flex-shrink: 0; }
@media (max-width: 1023px) {
  .btn-header { padding: 0 var(--s-3); }
  .btn-header .btn-label {
    position: absolute;
    width: 1px; height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
  }
}

.header-phone { display: none; }
@media (min-width: 1024px) { .header-phone { display: inline; } }
/* The minimal header (404, thank-you) carries no nav and no buttons, so there
   is room to keep the number visible at every width — and those are the two
   pages where a visitor most needs a way to reach someone. */
.site-header--minimal .header-phone { display: inline; }
/* Both are mobile-only: from 1024px the primary nav is visible and the phone
   number is a tel: link in the row, so neither is needed. */
@media (min-width: 1024px) {
  .header-actions .nav-toggle,
  .header-actions .call-icon { display: none; }
}

/* Mobile nav sheet */
.mobile-nav {
  position: fixed;
  inset: 0;
  z-index: 100;
  background: var(--c-bg);
  display: none;
  flex-direction: column;
  padding: var(--s-5);
}
.mobile-nav[data-open="true"] { display: flex; }
.mobile-nav-head { display: flex; justify-content: space-between; align-items: center; height: 40px; margin-bottom: var(--s-8); }
.mobile-nav ul { display: flex; flex-direction: column; }
.mobile-nav a {
  display: block;
  padding: var(--s-5) 0;
  font: 600 22px/1 var(--f-display);
  text-transform: uppercase;
  letter-spacing: 0.02em;
  color: var(--c-text);
  border-bottom: var(--bw-1) solid var(--c-border);
}
.mobile-nav a[aria-current="page"] { color: var(--c-accent-text); }
.mobile-nav-footer { margin-top: auto; padding-top: var(--s-8); }

/* ==========================================================================
   INVERTED CONTEXTS
   Everything below sits on a dark ground — a photo scrim or the dark footer —
   so its text must NOT follow the light-page tokens. Declared in one place so
   a future palette change can't silently turn any of it invisible. Scrim
   gradients themselves stay raw rgba(0,0,0,…): they darken a photograph, so
   they are genuinely black rather than a themed colour.
   ========================================================================== */
/* .tile-content is deliberately NOT here. The tile's label plate is
   --c-surface-3, a light grey — see the tile notes for why a dark plate was
   tried and reverted. If it ever goes dark again, it belongs in this list and
   .tile-count / .tile-summary / .tile-cta all need re-pointing with it. */
.hero-inner,
.page-hero--photo,
.band--dark,
.site-footer {
  color: var(--c-on-dark);
}
.hero-inner .h-display,
.hero-inner .h1,
.hero-inner .h2,
.page-hero--photo .h1,
.band--dark .h2,
.band--dark .h3,
.site-footer .h2,
.site-footer .h3 { color: var(--c-on-dark); }

.hero-inner .body-lg,
.hero-inner .body,
.page-hero--photo .body-lg,
.band--dark .body,
.band--dark .body-lg,
.site-footer .body { color: var(--c-on-dark-muted); }

.hero-inner .small,
.band--dark .small,
.site-footer .small,
.site-footer .footer-col-label,
.site-footer .footer-disclaimer,
.site-footer .footer-bottom { color: var(--c-on-dark-subtle); }

/* Amber as TEXT flips on a dark ground. --c-accent-text (#9A5B00) is the
   light-ground value at 5.4:1 on white; on #14171B it is 3.3:1 and fails AA.
   --c-accent (#F2A413) is the reverse — 2.1:1 on white, 8.6:1 here. This is the
   same split the palette comment describes, applied in the other direction, and
   it is why .btn-outline cannot simply be reused inside a dark band. */
.band--dark .eyebrow,
.band--dark .btn-outline { color: var(--c-accent); }
.band--dark .btn-outline { border-color: var(--c-accent); }
.band--dark .btn-outline:hover {
  background: rgba(var(--c-accent-rgb), 0.16);
  color: var(--c-accent);
}
/* .detail-ctas is built for the top of a catalog page: a full-width strip with
   its own --page-pad and a --c-border rule under it. Reused inside the closing
   band it would double the horizontal padding (the .container already applies
   it) and draw a #DDE1E4 line across the dark ground at 13.7:1 — a bright bar
   where a separator is not wanted. Reset all three, and stop the buttons
   growing to fill a 1200px container. */
.band--dark .detail-ctas {
  padding: 0;
  border-bottom: 0;
}
.band--dark .detail-ctas .btn { flex: 0 1 auto; }

.band--dark .text-link {
  color: var(--c-accent);
  border-bottom-color: var(--c-accent);
}
.band--dark .text-link:hover {
  color: var(--c-on-dark);
  border-bottom-color: var(--c-on-dark);
}

.hero-ctas { display: flex; gap: var(--s-2); flex-wrap: wrap; }
.hero-ctas .btn { flex: 1 1 auto; min-width: 160px; height: 48px; }
@media (min-width: 1024px) { .hero-ctas .btn { height: 56px; flex: 0 0 auto; padding: 0 var(--s-8); } }

/* real photography — set background-image inline on the element, as a dark
   overlay layer over the photo. The url() must be written inline rather than
   passed through a custom property: a relative url() inside a custom property
   resolves against this stylesheet (/css/), not the page. */
.photo {
  background-color: var(--c-photo-bed);
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
}
/* ---- Hero slider (home) ----
   A CSS scroll-snap track, so swipe and keyboard scrolling are native.
   Slides carry their own copy and CTAs. */
.hero-slider { position: relative; }
.hero-track {
  display: flex;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}
.hero-track::-webkit-scrollbar { display: none; }
.hero-slide {
  position: relative;
  flex: 0 0 100%;
  scroll-snap-align: center;
  min-height: 460px;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  padding: var(--s-8) var(--page-pad) var(--s-10);
  background-color: var(--c-photo-bed);
  overflow: hidden;               /* contains the Ken Burns overscale */
}
@media (min-width: 1024px) {
  .hero-slide { min-height: 560px; padding: var(--s-14) var(--page-pad) var(--s-14); }
}

/* The photo layer. A <picture> rather than a background-image, so each slide can
   send a portrait crop to phones and a wide one to desktop — the branded first
   slide is 16:9 with a full-width wordmark and a centre-crop would cut it to
   "ANTHI ENTERPRI". See tools/make-hero-images.ps1 for how the files are cut.
   It also gives the Ken Burns transform a target that isn't the text. */
.hero-media {
  position: absolute;
  inset: 0;
  z-index: 0;
  display: block;
}
.hero-media img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Two-layer scrim: a left-to-right darkening for the text column (copy is
   always left-aligned via .hero-inner, but the photo behind it changes —
   without this, busy detail directly under the headline fights the text
   regardless of how the photo is cropped) plus the original top-to-bottom
   one for the CTAs sitting at the bottom of the slide.
   Lighter than it once was: the parts-range and export artwork already carries
   its own dark panel on the left, and the old values stacked on top of that
   flattened the photo to mud. */
.hero-slide::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  background:
    linear-gradient(90deg, rgba(0,0,0,0.62) 0%, rgba(0,0,0,0.42) 40%, rgba(0,0,0,0.12) 66%, rgba(0,0,0,0) 88%),
    linear-gradient(rgba(0,0,0,0.30) 0%, rgba(0,0,0,0.42) 45%, rgba(0,0,0,0.82) 100%);
}
/* The branded slide has no overlay copy — the artwork carries the wordmark — so
   it takes no text scrim, just a shallow wash to keep the dots legible. */
.hero-slide--plain::after {
  background: linear-gradient(rgba(0,0,0,0) 70%, rgba(0,0,0,0.45) 100%);
}
.hero-inner { position: relative; z-index: 2; max-width: 620px; }

/* Motion below is gated on .is-ready, which initSlider() adds. Without JS the
   copy stays visible and the photo unscaled rather than stranded at opacity 0. */

/* Ken Burns: the active photo eases out of a slight overscale across the
   autoplay dwell. --hero-dwell is set from data-autoplay in js/main.js. */
.hero-slider.is-ready .hero-media img {
  transform: scale(1.075);
  transition: transform var(--hero-dwell, 7000ms) linear;
}
.hero-slider.is-ready .hero-slide.is-active .hero-media img { transform: scale(1); }

/* Copy entrance: eyebrow, headline, body and CTA rise in as their slide becomes
   active, and reset when it leaves so the run replays on the way round. */
.hero-slider.is-ready .hero-inner > * {
  opacity: 0;
  transform: translateY(14px);
}
.hero-slider.is-ready .hero-slide.is-active .hero-inner > * {
  opacity: 1;
  transform: none;
  transition: opacity var(--dur-slow) var(--ease-out), transform var(--dur-slow) var(--ease-out);
}
.hero-slider.is-ready .hero-slide.is-active .hero-inner > *:nth-child(1) { transition-delay: 60ms; }
.hero-slider.is-ready .hero-slide.is-active .hero-inner > *:nth-child(2) { transition-delay: 130ms; }
.hero-slider.is-ready .hero-slide.is-active .hero-inner > *:nth-child(3) { transition-delay: 200ms; }
.hero-slider.is-ready .hero-slide.is-active .hero-inner > *:nth-child(4) { transition-delay: 270ms; }
/* wide enough that the headline breaks where its <br> says it should,
   rather than wrapping mid-phrase */
@media (min-width: 1024px) { .hero-inner { max-width: 820px; } }
.hero-inner .eyebrow { display: block; margin-bottom: var(--s-3); }
.hero-inner .h-display { margin: 0; }
.hero-inner .body-lg {
  margin: var(--s-4) 0 var(--s-6);
  max-width: 440px;
  color: var(--c-on-dark-muted);
}
@media (min-width: 1024px) { .hero-inner .body-lg { max-width: 520px; } }

.hero-btn {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  z-index: 3;
  width: 44px; height: 44px;
  display: none;
  align-items: center;
  justify-content: center;
  background: rgba(10,11,13,0.7);
  border: var(--bw-1) solid var(--c-on-dark-border);
  color: var(--c-on-dark);
  font-size: 20px; line-height: 1;
  border-radius: var(--r-sm);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease-out), border-color var(--dur-fast) var(--ease-out);
}
@media (min-width: 768px) { .hero-btn { display: flex; } }
.hero-btn:hover { background: rgba(10,11,13,0.9); border-color: var(--c-accent); }
.hero-btn[hidden] { display: none; }
.hero-prev { left: var(--s-3); }
.hero-next { right: var(--s-3); }

.hero-dots {
  position: absolute;
  left: var(--page-pad);
  bottom: var(--s-5);
  z-index: 3;
  display: flex;
  gap: var(--s-2);
}
.hero-dot {
  position: relative;
  width: 36px; height: 4px;
  padding: 0; border: 0;
  background: rgba(242,244,245,0.35);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease-out);
}
.hero-dot[aria-current="true"] { background: rgba(242,244,245,0.5); }
/* The active dot fills across the autoplay dwell, so it doubles as a progress
   bar — the hero says how long it will sit rather than jumping unannounced.
   No overflow:hidden here: the fill is a scaleX inside its own bounds, and
   clipping would also cut the 44px touch target on ::before below. */
.hero-dot-fill {
  position: absolute;
  inset: 0;
  background: var(--c-accent);
  transform: scaleX(0);
  transform-origin: left center;
}
.hero-dot[aria-current="true"] .hero-dot-fill {
  animation: hero-dot-fill var(--hero-dwell, 7000ms) linear forwards;
}
/* Frozen in step with the timer — js/main.js puts .is-paused on the slider from
   stop()/start(), so the bar never keeps running while autoplay is halted. */
.hero-slider.is-paused .hero-dot-fill { animation-play-state: paused; }
@keyframes hero-dot-fill { from { transform: scaleX(0); } to { transform: scaleX(1); } }
.hero-dot::before {
  content: "";
  position: absolute;
  left: 0; width: 100%; height: 44px;
  top: 50%; transform: translateY(-50%);
}
@media (prefers-reduced-motion: reduce) {
  .hero-track { scroll-behavior: auto; }
  .hero-btn, .hero-dot { transition: none; }
  /* Autoplay is off under reduced motion, so the dot goes solid instead of
     filling. Specificity matches the animating rule above so it wins on order. */
  .hero-dot[aria-current="true"] .hero-dot-fill { animation: none; transform: scaleX(1); }
  .hero-slider.is-ready .hero-media img,
  .hero-slider.is-ready .hero-slide.is-active .hero-media img { transition: none; transform: none; }
  .hero-slider.is-ready .hero-inner > *,
  .hero-slider.is-ready .hero-slide.is-active .hero-inner > * {
    opacity: 1; transform: none; transition: none;
  }
}

.photo-caption {
  font: 500 11px/1.4 var(--f-body);
  letter-spacing: 0.04em;
  color: rgba(242,244,245,0.45);
  text-align: right;
  text-transform: uppercase;
}

/* ---- Brand strip (scrolling marquee) ----
   The track holds the brand list twice; translating it -50% lands exactly on
   the start of the second copy, so the loop is seamless. */
.brand-strip {
  padding: var(--s-4) 0;
  background: var(--c-surface-2);
  border-top: var(--bw-1) solid var(--c-border);
  border-bottom: var(--bw-1) solid var(--c-border);
  display: flex;
  align-items: center;
  gap: var(--s-5);
}
.brand-strip-label {
  font: 600 11px/1.3 var(--f-body);
  letter-spacing: 0.02em;
  color: var(--c-text-subtle);
  white-space: nowrap;
  flex-shrink: 0;
  padding-left: var(--page-pad);
}
.brand-marquee {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  /* fade both ends so brands enter and leave instead of being cut off */
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 32px, #000 calc(100% - 32px), transparent);
          mask-image: linear-gradient(90deg, transparent, #000 32px, #000 calc(100% - 32px), transparent);
}
.brand-track {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  width: max-content;
  animation: brand-scroll 55s linear infinite;
}
.brand-marquee:hover .brand-track,
.brand-marquee:focus-within .brand-track { animation-play-state: paused; }
@keyframes brand-scroll {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}
/* Each brand sits on a white plate. These logos come in every colour — some
   near-black (Caterpillar, Komatsu), some white knocked out of a coloured box
   (JCB, L&T, Tata Hitachi) — so a white plate is what renders every one in its
   true colours, and gives the text-only brands an identical shape to sit in.
   On the light strip the plate needs a hairline and a lift to separate from
   the band behind it. */
.brand-item {
  flex: 0 0 auto;
  height: 42px;
  padding: 0 var(--s-3);
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--c-surface);
  border: var(--bw-1) solid var(--c-border);
  border-radius: var(--r-sm);
  box-shadow: var(--shadow-1);
}
@media (min-width: 1024px) { .brand-item { height: 50px; } }
/* A fixed box with object-fit, rather than max-width/max-height: several of
   these SVGs carry no intrinsic width/height, so left to size themselves they
   collapse to the plate's content box. Letterboxing every mark into the same
   cell also keeps the plates a uniform width.

   The box's own aspect ratio (124/28 = 4.43) is built for a wide wordmark —
   Volvo, Hyundai, Caterpillar are all ratio 5-8 and fill it. But 11 of the 23
   marks are squarish BADGE logos (a roundel or a diamond, not a wordmark):
   Mitsubishi Motors and Cummins are ~1:1, Tata Hitachi ~2.4:1, Escorts ~1.1:1.
   object-fit: contain preserves aspect ratio, so a square mark in a 4.43:1 box
   is height-constrained and renders at as little as 21% of the cell's width —
   correct behaviour, wrong box. `.brand-item` (the cell) has always had more
   vertical room than this rule used (42/50px cell, 24/28px image) — raising
   the image's own height ceiling closer to the cell's spends that unused
   space on exactly the marks that need it. Wordmark logos are unaffected: a
   ratio-5+ mark is already width-constrained at 104/124px regardless of how
   much height is on offer, so this cannot make Volvo or Caterpillar smaller,
   only make Mitsubishi/Escorts/Tata Hitachi/Cummins/L&T/SDLG/Case bigger.
   (Ace, JCB and LeeBoy would also have grown here, but are pinned back to
   the original box by .brand-item--compact below, at the owner's request.) */
.brand-item img {
  width: 104px;
  height: 36px;
  object-fit: contain;
}
@media (min-width: 1024px) { .brand-item img { width: 124px; height: 44px; } }
/* Ace, JCB and LeeBoy opt back out of the taller box above, at the owner's
   request — reported too large once the squarish marks were given more room.
   Reverts to the original 104x24/124x28 box for exactly these three. Applied
   via the `compact: true` flag on the brand entry in js/main.js, not by
   targeting these three by name here, so the data stays the single source of
   truth for which marks are which size. */
.brand-item--compact img {
  height: 24px;
}
@media (min-width: 1024px) { .brand-item--compact img { height: 28px; } }
/* Brands whose logo is a copyrighted design we can't bundle show as wordmarks. */
.brand-item .brand-word {
  font: 700 13px/1 var(--f-display);
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--c-text);
  white-space: nowrap;
}
@media (min-width: 1024px) { .brand-item .brand-word { font-size: 15px; } }

@media (prefers-reduced-motion: reduce) {
  .brand-track { animation: none; }
  .brand-track [data-brand-dup] { display: none; }
  .brand-marquee { overflow-x: auto; -webkit-mask-image: none; mask-image: none; }
}

/* ---- Section bands ----
   A full-bleed ground behind a section, so a long page has landmarks instead of
   one continuous white column. Export stacks eleven hairline cards in a row and
   About nine; --c-surface-2 was in the palette described as a "subtle band" and
   was never once used to band anything.

   It has to wrap OUTSIDE .container to reach the window edges, so the markup
   closes the container, wraps, and reopens. Padding still comes from .section —
   a band sets ground colour and nothing else. Keep .reveal on the <section>:
   the observer is a flat querySelectorAll(".reveal") in js/main.js, so the
   wrapper is invisible to it.

   Bonus, and the actual fix for the stacked cards: .card is --c-surface, which
   equals --c-bg. On white they are white-on-white separated by a hairline; on a
   band they finally read as cards. */
.band { width: 100%; }
.band--tint { background: var(--c-surface-2); }
.band--dark { background: var(--c-footer-bg); }

/* ---- Section header ---- */
/* scroll-margin-top matches .site-header's own height (sticky, 56/74px) so an
   in-page jump to any #id inside a .section — a part-group anchor, or the new
   home -> products.html#categories deep link — doesn't land half-hidden under
   the fixed header. Global here rather than per-anchor, since every current
   and future jump target on the site sits inside a .section. */
.section {
  padding: var(--s-8) 0;
  scroll-margin-top: 56px;
}
@media (min-width: 1024px) {
  .section { padding: var(--s-20) 0; scroll-margin-top: 74px; }
}
.section-head {
  margin-bottom: var(--s-6);
}
.section-head h2 { margin-top: var(--s-2); }
@media (min-width: 1024px) {
  .section-head { display: flex; align-items: flex-end; justify-content: space-between; gap: var(--s-8); margin-bottom: var(--s-8); }
  .section-head .body { max-width: 340px; text-align: right; }
}
/* A short accent rule marking the start of every section. Same idiom as the
   3px accent border already on .answer-block and .cta-card, turned horizontal.
   It hangs off the first child rather than off .section-head itself, because
   .section-head becomes a flex row at 1024px and a ::before there would land in
   the row as a third item. `> :first-child` rather than `> div:first-child`:
   three section heads on the generated pages hold a bare <h2> with no wrapping
   div ("Brand coverage", "Other categories", "Other machines"), and those would
   otherwise be the only sections on the site without the mark. */
.section-head > :first-child::before {
  content: "";
  display: block;
  width: 32px;
  height: var(--bw-3);
  background: var(--c-accent);
  margin-bottom: var(--s-3);
}

/* ---- Tile grid (categories) ---- */
.tile-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 10px;
}
@media (min-width: 768px) { .tile-grid { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 1024px) { .tile-grid { grid-template-columns: repeat(4, 1fr); gap: 12px; } }

/* Tiles are LIGHT: the photo is shown clean and the label sits in an opaque bar
   across the foot. They used to be a full-bleed photo under a black scrim
   (0.35 → 0.85), which was right when these tiles carried dark jobsite
   photographs. Every catalog image is now a studio product shot on a white
   sweep — see tools/make-category-images.ps1 — and that scrim rendered the top
   of each tile at ~#A6A6A6 and the foot at ~#262626, greying out the whole
   point of the photography. Nothing about the markup changed; it is generated
   in three places (js/catalog-grids.js and two builders in
   tools/lib/render.mjs) and keeping this in CSS means they cannot drift. */
.tile {
  position: relative;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background-color: var(--c-surface);
  border: var(--bw-1) solid var(--c-border);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-1);
  transition: border-color var(--dur-fast) var(--ease-out), box-shadow var(--dur-fast) var(--ease-out);
}
/* The photo owns its box and the label bar adds height BELOW it, rather than
   the tile owning the aspect and the bar overlaying the photo inside it. That
   alternative does not survive a phone: a 2-col grid at 375px gives a 163x122
   tile, and a category bar with a two-line name ("Ground Engaging Tools") is
   112px of it — 10px of visible photo. In flow the bar can never eat the
   picture.

   THE BOX MATCHES THE SOURCE RATIO, so background-size:cover crops nothing.
   Category images are 900x600 (exactly 3:2) and machine images 900x720
   (exactly 5:4) — see the two make-*-images.ps1 scripts, which also fit-and-pad
   rather than crop, so the subject already runs close to the frame edge. A
   shared 4:3 box took 45px off the top and bottom of every machine and 100px
   off the sides of every category photo. Do not "tidy" these to one value.

   flex-grow stays off for the same reason: when grid row-stretch makes a
   neighbouring tile taller, growing the photo would break its ratio and start
   cropping again. margin-top:auto on the bar sends the slack above it instead.

   The negative margins put the photo under the 1px border, which is what
   inset:-1px did before — without them a hairline of tile background shows at
   the top and sides. */
.tile-bg {
  flex: 0 0 auto;
  aspect-ratio: 3 / 2;
  margin: -1px -1px 0;
  transition: transform var(--dur-slow) var(--ease-out);
}
.tile-eq .tile-bg { aspect-ratio: 5 / 4; }
/* The label plate is --c-surface-3, one step darker than the section band.
   Sizing this correctly needs ΔL* (perceptual lightness), NOT a WCAG contrast
   ratio — WCAG measures whether TEXT is legible on a ground, and using it to
   size the boundary between two large flat surfaces massively overshoots. A
   ΔL* of 3-4 already reads as a clean edge:

     plate            ΔL* from photo   ΔL* from a --c-surface-2 band
     #FFFFFF (was)          0.0                 4.3    invisible against photo
     #F1F3F5                4.3                 0.0    merges into the band
     #E4E8EA  <- this       8.2                 4.0    both edges read
     #14171B                92.4                88.2   ~25x more than needed

   That last row was shipped briefly and looked like 22 black slabs; the number
   driving it was 18:1, a text metric applied to a surface problem.

   Both grounds matter because these grids sit on both: machine tiles are always
   on white, category tiles are on a --c-surface-2 band on products.html and on
   every /machines/ page. One plate value has to work in both places.

   The hairline stays. At ΔL* 8.2 the tone change alone is a soft edge, and the
   border-top makes it crisp without adding weight. */
.tile-content {
  flex: 0 0 auto;
  margin-top: auto;
  padding: var(--s-2) var(--s-3) var(--s-3);
  color: var(--c-text);
  background: var(--c-surface-3);
  border-top: var(--bw-1) solid var(--c-border);
}
@media (min-width: 1024px) { .tile-content { padding: var(--s-3) var(--s-5) var(--s-4); } }

/* The code and arrow sit ON the photo rather than in the label bar, which takes
   ~14px off every tile in the catalog.

   Solid pills, NOT the white veil the phone hero uses. The hero can veil
   because its headline is 30px and holds 5.7:1 even where the gradient has
   faded to 0.57. This code is 10px in --c-accent-text, which needs the ground
   at 0.92 white before it reaches 4.5:1 — and a 0.92 veil across the top of the
   tile is just a second opaque bar, which is the thing this change removes. A
   pill is 5.4:1 and covers nothing but itself. */
.tile-top {
  position: absolute;
  top: var(--s-2); left: var(--s-2); right: var(--s-2);
  z-index: 1;
  display: flex; justify-content: space-between; align-items: center;
  gap: var(--s-2);
}
.tile-code,
.tile-arrow {
  background: var(--c-surface);
  border: var(--bw-1) solid var(--c-border);
  border-radius: var(--r-sm);
}
.tile-code { padding: 5px 7px; }
.tile-arrow {
  width: 22px; height: 22px;
  display: flex; align-items: center; justify-content: center;
  font-size: 13px; line-height: 1;
}
.tile-code { font: 500 10px/1 var(--f-mono); letter-spacing: 2px; color: var(--c-accent-text); }
.tile-arrow { color: var(--c-text-subtle); transition: color var(--dur-fast) var(--ease-out); }
.tile-name { font: 600 17px/1.05 var(--f-display); letter-spacing: 0.02em; text-transform: uppercase; }
@media (min-width: 1024px) { .tile-name { font-size: 24px; } }
.tile-summary { font: 400 11px/1.4 var(--f-body); color: var(--c-text-muted); margin-top: var(--s-1); }
/* Always visible. It used to fade in on hover, which is fine over a fixed-size
   scrim but would grow the label bar and, now that the bar is in flow, shove
   the photo on hover. On a light bar it reads as the link cue it always was.

   NOTE: not currently rendered inside a .tile — it was dropped from the
   category tiles and now only appears in .fallback-cta, which sits on white
   (5.4:1). If you put it back in a tile, --c-accent-text on the
   --c-surface-3 plate is 4.4:1, just under AA. Darken it or lighten the plate
   for that case; do not ship it as-is. */
.tile-cta {
  font: 600 11px/1 var(--f-body); letter-spacing: 0.15em; text-transform: uppercase;
  color: var(--c-accent-text);
  margin-top: var(--s-2);
  transition: color var(--dur-fast) var(--ease-out);
}

.tile:hover { border-color: var(--c-accent); box-shadow: inset 0 -4px 0 var(--c-accent), var(--shadow-2); }
.tile:hover .tile-bg { transform: scale(1.07); }
.tile:hover .tile-arrow { color: var(--c-accent-text); }
.tile:active { box-shadow: none; }
.tile:active .tile-bg { transform: scale(1.03); }

@media (prefers-reduced-motion: reduce) {
  .tile, .tile-bg, .tile-arrow, .tile-cta { transition: none; }
  .tile:hover .tile-bg, .tile:active .tile-bg { transform: none; }
}

/* ---- Equipment tiles (catalog level 1) ----
   Deliberately picture-first: a buyer should recognise their machine from the
   photo alone, so the name is set larger and there is no summary line. The
   4:3 frame now comes from .tile itself — category sources are 3:2 and machine
   sources 5:4, and a square tile was taking 33% off the width of every
   category photo. */
.eq-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 10px;
}
@media (min-width: 768px) { .eq-grid { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 1024px) { .eq-grid { grid-template-columns: repeat(4, 1fr); gap: 12px; } }
.tile-eq .tile-name { font-size: 19px; }
@media (min-width: 1024px) { .tile-eq .tile-name { font-size: 26px; } }
.tile-count {
  font: 500 12px/1.3 var(--f-body);
  letter-spacing: 0.01em;
  color: var(--c-text-muted);
  margin-top: var(--s-1);
}

/* ---- Detail-page CTA row (category + equipment pages) ----
   Wraps to one button per line on narrow screens; min-width:0 lets the long
   WhatsApp label shrink instead of pushing the row off-screen. */
.detail-ctas {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
  padding: var(--s-4) var(--page-pad);
  border-bottom: var(--bw-1) solid var(--c-border);
}
.detail-ctas .btn { flex: 1 1 220px; min-width: 0; height: 46px; }

/* ---- Fallback CTA row ---- */
.fallback-cta {
  display: flex; align-items: center; justify-content: space-between; gap: var(--s-4);
  padding: var(--s-5);
  background: var(--c-surface);
  border: var(--bw-1) solid var(--c-border);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-1);
  color: var(--c-text);
  margin-top: var(--s-4);
  transition: border-color var(--dur-fast) var(--ease-out), box-shadow var(--dur-fast) var(--ease-out);
}
.fallback-cta:hover { border-color: var(--c-accent); box-shadow: var(--shadow-2); }
@media (prefers-reduced-motion: reduce) { .fallback-cta { transition: none; } }
.fallback-cta strong { display: block; font: 600 15px/1.1 var(--f-display); text-transform: uppercase; letter-spacing: 0.02em; }
.fallback-cta .small { margin-top: var(--s-1); }
/* .tile-cta is amber for a dark tile scrim; inside the light fallback-cta it
   needs the darker accent to clear contrast. */
.fallback-cta .tile-cta { opacity: 1; transform: none; margin-top: 0; flex-shrink: 0; color: var(--c-accent-text); }

/* ---- Footer ----
   Deliberately stays dark on the light page: it anchors the bottom, keeps the
   brand present, and lets the footer go on using the dark logo lockup. All its
   text comes from the --c-on-dark family (see INVERTED CONTEXTS above). */
.site-footer {
  background: var(--c-footer-bg);
  border-top: var(--bw-1) solid var(--c-footer-bg);
  padding: var(--s-10) 0 var(--s-8);
}
/* Four columns: brand, By machine, By part category, Company. The two catalog
   columns are deliberately separate — a single "Products" column carrying all
   twelve categories ran 14 items deep beside a 2-item Export column, and listed
   no machines at all. A 2-up step at 640px keeps the link columns from becoming
   four long ribbons on a tablet; the brand spans both there. */
.footer-grid { display: grid; gap: var(--s-8); }
@media (min-width: 640px) {
  .footer-grid { grid-template-columns: repeat(2, 1fr); gap: var(--s-8) var(--s-6); }
  .footer-brand-col { grid-column: 1 / -1; }
}
@media (min-width: 1024px) {
  .footer-grid { grid-template-columns: 1.5fr 1fr 1.1fr 0.9fr; gap: var(--s-8) var(--s-6); }
  .footer-brand-col { grid-column: auto; }
}
.footer-brand { display: flex; align-items: center; gap: var(--s-3); margin-bottom: var(--s-4); }
.footer-col-label { font: 600 12px/1 var(--f-body); letter-spacing: 0.04em; color: var(--c-on-dark-subtle); margin-bottom: var(--s-3); text-transform: uppercase; }

/* The blanket link colour comes FIRST, so the specific rules below can override
   it. Written the other way round it silently wins: `.site-footer a` is (0,1,1)
   and `.footer-links a` is also (0,1,1), so equal specificity hands it to
   whichever is later in the file. The descendant forms below are (0,2,1) and
   (0,2,0), which beat it on specificity rather than on luck. */
.site-footer a { color: var(--c-on-dark); }
.site-footer a:hover { color: var(--c-accent); }

/* Real lists. These were a run of <a> separated by <br> inside a <p>, which
   gave a screen reader no item count and no way to skip the group. */
.footer-links { list-style: none; margin: 0; padding: 0; }
.footer-links li { margin-bottom: var(--s-2); }
.site-footer .footer-links a {
  font: 400 13px/1.35 var(--f-body);
  color: var(--c-on-dark-muted);
  display: inline-block;
}
.site-footer .footer-links a:hover { color: var(--c-accent); }

/* Tappable. It used to be a <div> holding a number you could not dial. */
.footer-phone {
  display: inline-block;
  margin-top: var(--s-5);
  font: 600 20px/1.2 var(--f-display);
  letter-spacing: 0.02em;
}
.footer-note { margin-top: var(--s-4); }
.site-footer .footer-cta-link { color: var(--c-accent); }
.site-footer .footer-cta-link:hover { color: var(--c-on-dark); }
.footer-disclaimer {
  margin-top: var(--s-8);
  padding-top: var(--s-6);
  border-top: var(--bw-1) solid rgba(242,244,245,0.12);
  font: 400 11px/1.6 var(--f-body);
  color: var(--c-on-dark-subtle);
  max-width: 760px;
}
.footer-bottom { margin-top: var(--s-6); font: 400 11px/1.6 var(--f-body); color: var(--c-on-dark-subtle); }

/* ---- Sticky WhatsApp bar (mobile) ---- */
.sticky-bar {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  z-index: 80;
  display: flex;
  gap: 1px;
  background: var(--c-border);
  border-top: var(--bw-1) solid var(--c-border);
  box-shadow: var(--shadow-up);
  transform: translateY(0);
  transition: transform var(--dur-fast) var(--ease-out);
}
.sticky-bar[data-hidden="true"] { transform: translateY(100%); }
.sticky-bar .btn { flex: 1; height: 56px; border: 0; }
@media (min-width: 1024px) { .sticky-bar { display: none; } }
@media (prefers-reduced-motion: reduce) { .sticky-bar { transition: none; } }

/* ---- Reveal-on-scroll ---- */
.reveal { opacity: 0; transform: translateY(16px); transition: opacity 420ms var(--ease-out), transform 420ms var(--ease-out); }
.reveal.is-visible { opacity: 1; transform: none; }
@media (prefers-reduced-motion: reduce) {
  .reveal { opacity: 1; transform: none; transition: none; }
}

/* ---- Forms (enquiry modal + contact page) ---- */
.field { display: flex; flex-direction: column; gap: 6px; }
.field-label { font: 500 12px/1.3 var(--f-body); letter-spacing: 0.01em; color: var(--c-text-subtle); }
.field input,
.field select,
.field textarea {
  background: var(--c-surface);
  border: var(--bw-1) solid var(--c-border-strong);
  border-radius: var(--r-sm);
  color: var(--c-text);
  min-height: 44px;
  padding: 0 var(--s-3);
  font: 500 14px/1 var(--f-body);
}
.field textarea { min-height: 74px; padding: var(--s-3); font-weight: 400; line-height: 1.5; resize: vertical; }
.field input::placeholder,
.field textarea::placeholder { color: var(--c-text-subtle); opacity: 0.75; font-weight: 400; }
.field input:focus,
.field select:focus,
.field textarea:focus { border-color: var(--c-accent); outline: none; }
.field[data-filled="true"] input,
.field[data-filled="true"] select { border-color: var(--c-accent); }
.field-row { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s-2); }
/* Grid items default to min-width:auto, so a <select> with long option text
   (the category list) pushed its column past 1fr and overflowed the page on
   narrow screens. Letting the columns shrink keeps the row inside the form. */
.field-row > * { min-width: 0; }
.field select, .field input, .field textarea { max-width: 100%; }
.field-error { font: 400 11px/1.4 var(--f-body); color: var(--c-danger-500); display: none; }
.field[data-invalid="true"] input,
.field[data-invalid="true"] select { border-color: var(--c-danger-500); }
.field[data-invalid="true"] .field-error { display: block; }

.form-grid { display: flex; flex-direction: column; gap: var(--s-3); }
.form-ctas { display: flex; gap: var(--s-2); margin-top: var(--s-1); }
.form-ctas .btn { flex: 1; height: 50px; }
.form-footnote { font: 400 11px/1.5 var(--f-body); color: var(--c-text-subtle); margin-top: var(--s-2); }

/* ---- Enquiry modal ---- */
.modal-backdrop {
  position: fixed; inset: 0;
  background: rgba(20,23,27,0.55);
  z-index: 150;
  display: none;
}
.modal-backdrop[data-open="true"] { display: block; }

.modal {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  z-index: 151;
  max-height: 85vh;
  overflow-y: auto;
  background: var(--c-surface);
  color: var(--c-text);
  border-top: var(--bw-3) solid var(--c-accent);
  border-radius: var(--r-lg) var(--r-lg) 0 0;
  box-shadow: var(--shadow-3);
  display: none;
  transform: translateY(100%);
  transition: transform var(--dur-modal) var(--ease-out);
}
.modal[data-open="true"] { display: block; }
.modal[data-animate-in="true"] { transform: translateY(0); }

@media (min-width: 768px) {
  .modal {
    left: 50%; right: auto; bottom: auto; top: 50%;
    width: min(440px, calc(100vw - 40px));
    border-radius: var(--r-lg);
    transform: translate(-50%, -50%);
    opacity: 0;
    transition: opacity 160ms var(--ease-out);
  }
  .modal[data-animate-in="true"] { opacity: 1; transform: translate(-50%, -50%); }
}
@media (prefers-reduced-motion: reduce) {
  .modal { transition: opacity 160ms linear; transform: none !important; }
}

.modal-head {
  padding: var(--s-5) var(--s-5) var(--s-1);
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--s-4);
}
.modal-close {
  width: 36px; height: 36px;
  border: var(--bw-1) solid var(--c-border-strong);
  border-radius: var(--r-sm);
  display: grid; place-items: center;
  color: var(--c-text-muted);
  font-size: 20px;
  flex-shrink: 0;
}
.modal-sub { padding: var(--s-3) var(--s-5) 0; }
.modal-body { padding: var(--s-5); }

.modal-success { padding: var(--s-8) var(--s-5); text-align: center; display: none; }
.modal-success[data-active="true"] { display: block; }
.modal-success svg { margin: 0 auto var(--s-4); display: block; }

/* ---- Page hero (sub-pages) ---- */
.page-hero {
  padding: var(--s-10) 0 var(--s-8);
  border-bottom: var(--bw-1) solid var(--c-border);
}
@media (min-width: 1024px) { .page-hero { padding: var(--s-20) 0 var(--s-10); } }

/* Split variant — the 22 machine and category pages. Copy on one side, the
   product shot in a panel on the other, with nothing overlapping the photo.
   NOT .page-hero--photo: that lays white text on a black scrim, which works on
   the export page's port photograph and cannot work here. Every catalog image
   is a studio shot on a white sweep, so a scrim heavy enough to make white text
   legible is exactly the scrim that destroys the product shot.

   The <h1> stays first in the DOM; the figure is lifted above it visually on
   phones only, so reading and heading order are untouched. */
.page-hero--split .page-hero-grid {
  display: flex;
  flex-direction: column;
  gap: var(--s-5);
  position: relative;
}
.page-hero--split .page-hero-figure { order: -1; margin: 0; }

/* PHONES ONLY: the heading floats over the picture and the hero runs full
   bleed, which is where the vertical saving actually is. On desktop the split
   hero's height is already set by the image panel — the copy column is shorter
   and centred beside it — so overlaying there would save nothing and only cost
   legibility. Hence max-width, not a new modifier class.

   Only .page-hero-head goes over the image. The lede stays in flow below it:
   measured at 375px, a category lede is up to 314px of text over a 250px-tall
   image. The heading alone is 110px at worst.

   The veil is WHITE, not the black scrim used on the export page. Measured
   across all 22 files, the top-left zone runs 90–228 mean luminance with
   12–81% of pixels below 180, so dark text needs lifting almost everywhere —
   but machine art is bright up there (the machine sits low in frame), so a
   white veil is nearly invisible on the ten machine pages and only touches the
   top of the twelve parts grids. A black scrim would darken every one of them.
   Held at ~0.88 across the full height of the text: over even a pure black
   pixel that is 224, which keeps --c-text above 12:1. */
@media (max-width: 767px) {
  .page-hero--split { padding: 0 0 var(--s-6); }
  .page-hero--split > .container { padding-left: 0; padding-right: 0; }
  .page-hero--split .page-hero-grid { gap: 0; }
  .page-hero--split .page-hero-figure img { border: 0; border-radius: 0; }
  .page-hero--split .page-hero-head {
    position: absolute;
    top: 0; left: 0; right: 0;
    z-index: 1;
    padding: var(--s-5) var(--page-pad) var(--s-10);
    background: linear-gradient(180deg,
      rgba(255,255,255,0.94) 0%,
      rgba(255,255,255,0.90) 62%,
      rgba(255,255,255,0.60) 84%,
      rgba(255,255,255,0) 100%);
  }
  .page-hero--split .page-hero-lede {
    padding: var(--s-5) var(--page-pad) 0;
    margin: 0;
  }
}

@media (min-width: 768px) {
  .page-hero--split .page-hero-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    align-items: center;
    gap: var(--s-8);
  }
  .page-hero--split .page-hero-figure { order: 0; }
  .page-hero--split .page-hero-lede { margin-top: var(--s-3); max-width: 60ch; }
}
@media (min-width: 1024px) {
  /* The copy column takes the larger share, and the split hero runs shorter
     than the text-only one. A 4:3 panel at half of a 1200px container is
     ~420px tall on its own, so keeping .page-hero's full desktop padding would
     put the first section well below the fold. */
  .page-hero--split { padding: var(--s-14) 0 var(--s-10); }
  .page-hero--split .page-hero-grid { grid-template-columns: 1.2fr 1fr; gap: var(--s-10); }
}
/* No aspect-ratio and no object-fit: the width/height attributes on the <img>
   carry the file's own ratio, so the panel is never cropped and still reserves
   its space before the file lands. Machine art is 5:4 and category art 3:2, so
   forcing one shared box here cropped one of them by definition.
   The load-time bed is light, not --c-photo-bed: these are studio shots on a
   white sweep, and a dark rectangle flashing behind them reads as a fault. */
.page-hero-figure img {
  display: block;
  width: 100%;
  height: auto;
  border: var(--bw-1) solid var(--c-border);
  border-radius: var(--r-md);
  background-color: var(--c-surface-2);
}

/* Photo variant. Only the export page uses this — .page-hero is shared by 27
   pages and stays plain text on white. It reuses .hero-media from the home
   slider for the image layer, so there is one implementation of "photo behind
   copy" rather than two. */
.page-hero--photo {
  position: relative;
  overflow: hidden;
  border-bottom: 0;
  background-color: var(--c-photo-bed);
  min-height: 300px;
  display: flex;
  align-items: flex-end;
  padding: var(--s-14) 0 var(--s-8);
}
@media (min-width: 1024px) {
  .page-hero--photo { min-height: 420px; padding: var(--s-20) 0 var(--s-14); }
}
/* Same two-layer logic as the home slider: left-to-right for the copy column,
   bottom-weighted for the text sitting on the floor of the frame. */
.page-hero--photo::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  background:
    linear-gradient(90deg, rgba(0,0,0,0.72) 0%, rgba(0,0,0,0.5) 45%, rgba(0,0,0,0.15) 72%, rgba(0,0,0,0) 92%),
    linear-gradient(rgba(0,0,0,0.25) 0%, rgba(0,0,0,0.4) 50%, rgba(0,0,0,0.75) 100%);
}
.page-hero--photo > .container { position: relative; z-index: 2; width: 100%; }

/* Card rhythm. Pages currently set these with inline margin-top on nearly every
   element. These rules make that unnecessary for new markup; inline styles
   still win where they already exist, so nothing shipping changes. */
.card > .eyebrow + .h3 { margin-top: var(--s-3); }
.card > .h3 + .body,
.card > .eyebrow + .body { margin-top: var(--s-2); }
.card > .body + .tag-list,
.card > .body + .cta-actions { margin-top: var(--s-4); }

/* The closing call to action should read as the end of the page, not as one
   more card in the stack. */
/* About page warehouse band. This is the one genuinely real photograph on the
   site — the owner's own premises — so it gets room and a caption saying so,
   rather than the 220px decorative strip it used to be. An <img> rather than a
   CSS background: the file sits below the fold, and a background cannot be
   lazy-loaded, so every visitor was pulling 535 KB they might never scroll to. */
.about-photo { position: relative; margin: 0; }
.about-photo img {
  width: 100%;
  height: 260px;
  object-fit: cover;
  object-position: center 38%;
  display: block;
  border-radius: var(--r-md);
  background-color: var(--c-photo-bed);
}
@media (min-width: 768px) { .about-photo img { height: 360px; } }
.about-photo figcaption {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  padding: var(--s-6) var(--s-5) var(--s-4);
  border-radius: 0 0 var(--r-md) var(--r-md);
  background: linear-gradient(rgba(0,0,0,0) 0%, rgba(0,0,0,0.72) 100%);
  color: var(--c-on-dark);
  font: 500 12px/1.4 var(--f-body);
}

/* Inline links inside body copy. The site sets a:{color:inherit;
   text-decoration:none} globally, so a link in a paragraph is invisible unless
   it says otherwise — this was being done with a repeated inline style. */
.text-link {
  color: var(--c-accent-text);
  border-bottom: var(--bw-1) solid var(--c-accent-text);
}
.text-link:hover { color: var(--c-text); border-bottom-color: var(--c-text); }

.cta-card { border-left: var(--bw-3) solid var(--c-accent); }
.cta-actions { display: flex; gap: var(--s-2); flex-wrap: wrap; }

/* ---- Breadcrumb ---- */
.breadcrumb {
  padding: var(--s-3) 0;
  border-bottom: var(--bw-1) solid var(--c-border);
  font: 400 12px/1.4 var(--f-body);
  letter-spacing: 0.01em;
  color: var(--c-text-muted);
}
.breadcrumb ol { display: flex; flex-wrap: wrap; align-items: center; }
.breadcrumb li { display: flex; align-items: center; }
/* Separator is decorative — the <ol> already conveys the hierarchy. The `/ ""`
   is CSS Speech alt text, which stops screen readers announcing "slash" on
   every hop; browsers without support just read it as they do today. */
.breadcrumb li + li::before {
  content: "/" / "";
  margin: 0 var(--s-2);
  color: var(--c-border-strong);
}
.breadcrumb a { color: var(--c-text-muted); }
.breadcrumb a:hover { color: var(--c-text); }
.breadcrumb .current { color: var(--c-accent-text); }

/* ---- Cards (used on about / export) ---- */
.card {
  background: var(--c-surface);
  border: var(--bw-1) solid var(--c-border);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-1);
  padding: var(--s-6);
}
.card + .card { margin-top: var(--s-3); }
.card-grid { display: grid; gap: var(--s-3); grid-template-columns: 1fr; }
/* inside a grid the gap already spaces them — the stacking margin above would
   otherwise push every card after the first down and break the top alignment */
.card-grid .card + .card { margin-top: 0; }
@media (min-width: 768px) { .card-grid { grid-template-columns: repeat(2, 1fr); } }

/* Numbered process steps. The 01/02/03 were 11px mono eyebrows — the same size
   as every other label on the page, so a sequence read as three unrelated
   cards. Set large in amber they carry both the ordering and most of the colour
   on these pages, out of content that was already there. --c-accent-text, not
   --c-accent: this is text on a light card, where the fill amber is 2.1:1. */
.card-grid--steps .eyebrow--code {
  display: block;
  font: 600 34px/1 var(--f-display);
  letter-spacing: -0.01em;
  color: var(--c-accent-text);
}
@media (min-width: 1024px) { .card-grid--steps .eyebrow--code { font-size: 44px; } }
.card-grid--steps .card > .eyebrow + .h3 { margin-top: var(--s-2); }
@media (min-width: 1024px) { .card-grid.cols-3 { grid-template-columns: repeat(3, 1fr); } }

/* Contact cards. The supporting line varies in length between the three, so
   without this the buttons sit at three different heights and the row reads
   ragged; margin-top:auto pins them to a common baseline. */
.contact-card { display: flex; flex-direction: column; }
.contact-card .btn { margin-top: auto; align-self: flex-start; }
.contact-card .small { margin-bottom: var(--s-4); }
/* The address is long enough to overflow a narrow column, and .h3 is a display
   face sized for short headings. Let it wrap on the character if it must. */
.contact-email { font-size: 17px; line-height: 1.25; overflow-wrap: anywhere; }

.tag-list { display: flex; flex-wrap: wrap; gap: var(--s-2); }
.tag {
  font: 500 11px/1 var(--f-body);
  padding: var(--s-2) var(--s-3);
  border: var(--bw-1) solid var(--c-border-strong);
  border-radius: var(--r-sm);
  background: var(--c-surface);
  color: var(--c-text-muted);
}
/* Tags that link to a machine (category.html) need a real tap target;
   plain span.tag chips — the brand list on about.html — stay as they were. */
a.tag {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 0 var(--s-4);
  text-decoration: none;
  transition: border-color var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out);
}
a.tag:hover { border-color: var(--c-accent); color: var(--c-text); }
a.tag[aria-current="page"] { border-color: var(--c-accent); color: var(--c-accent-text); }
@media (prefers-reduced-motion: reduce) { a.tag { transition: none; } }

/* The home page's category list is a section's main content, not a dense
   footnote strip like the brand chips on about.html, so the base 11px chip is
   undersized for it. This variant is the same component one step up: 13px,
   roomier padding, and a little more air between chips.
   `.tag`'s ground is --c-surface (white), which is why this belongs on the
   tint band — on plain white the chips would be white-on-white, readable only
   by their hairline border, the same problem bands were introduced to fix for
   .card. Do not move it onto an untinted section without changing the chip
   ground too. */
.tag-list--wide { gap: var(--s-3); margin-bottom: var(--s-2); }
.tag-list--wide .tag {
  font-size: 13px;
  color: var(--c-text);
}
/* Padding is set per element type here, NOT on .tag-list--wide .tag, and that
   is deliberate. a.tag above pairs `padding: 0 var(--s-4)` with
   `min-height: 44px` — the two together are what make the 44px tap target. A
   blanket padding on the variant would have beaten a.tag on specificity
   ((0,2,0) over (0,1,1)) and quietly taken that pairing apart.
   Child combinator so this can only ever match a chip that is a direct item of
   the list, never something nested inside one. */
.tag-list--wide > span.tag { padding: var(--s-3) var(--s-4); }

/* The line count, same idiom as .submenu-count in the products dropdown: mono,
   subtle, trailing the name. It turns thirteen identical outlined labels into
   thirteen facts — "Hydraulic Parts 77" says something "Hydraulic Parts" does
   not, and it is the one number on this page that shows the inventory is deep
   rather than just broad. Sourced from c.partCount at build time, so it tracks
   the catalog. */
.tag-count {
  margin-left: var(--s-2);
  font: 400 11px/1 var(--f-mono);
  color: var(--c-text-subtle);
}
a.tag:hover .tag-count { color: var(--c-text-muted); }

/* ---- Placeholder note (dev-facing, visually subtle) ---- */
.placeholder-note {
  border: 1px dashed var(--c-border-strong);
  color: var(--c-text-subtle);
  font: 400 12px/1.5 var(--f-body);
  padding: var(--s-3) var(--s-4);
}

/* ---- 404 ---- */
.error-page {
  min-height: 60vh;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
  gap: var(--s-4);
}

/* ================================================================
   CATALOG — parts list, sub-group accordion, answer block
   Added for the 561-part catalog. Three components that did not
   exist before: a part row, a disclosure, and a lead-in paragraph.
   ================================================================ */

/* ---- Answer block ----
   The first thing under the H1 on every catalog page, and deliberately a
   complete sentence naming the company, the parts and the machines. An AI
   assistant retrieves a fragment rather than a page, so the fragment has to
   identify the business on its own to be quotable. */
.answer-block {
  border-left: 3px solid var(--c-accent);
  padding: var(--s-3) 0 var(--s-3) var(--s-4);
  margin: var(--s-5) 0;
  font: 400 16px/1.55 var(--f-body);
  color: var(--c-text);
  max-width: 68ch;
}
@media (min-width: 1024px) { .answer-block { font-size: 17px; } }
.answer-block strong { font-weight: 600; }

/* ---- Part list ----
   Deliberately a list, not a <table>. The data is a set of products with
   attributes, not a comparison matrix: nobody reads down the "fits" column
   comparing values, they scan the names for their part. A list reflows from
   360px to 1280px with no horizontal scroll, which no 3-column table does. */
.part-list { display: flex; flex-direction: column; gap: 0; }

.part-row {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--s-1) var(--s-4);
  padding: var(--s-3) 0;
  border-bottom: var(--bw-1) solid var(--c-border);
}
.part-row:last-child { border-bottom: 0; }

.part-name {
  font: 600 15px/1.3 var(--f-body);
  color: var(--c-text);
  margin: 0;
}
.part-desc {
  font: 400 13px/1.45 var(--f-body);
  color: var(--c-text-muted);
  margin: 0;
}
.part-fits {
  font: 400 12px/1.4 var(--f-body);
  letter-spacing: 0.01em;
  color: var(--c-text-subtle);
}
/* The owner's own hedge ("some models — confirm on enquiry"), preserved rather
   than flattened into a compatibility claim they did not make. */
.part-fits-note { text-transform: none; letter-spacing: 0.02em; font-style: italic; }

.part-flag {
  display: inline-block;
  font: 500 10px/1 var(--f-mono);
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--c-accent-text);
  border: var(--bw-1) solid var(--c-accent);
  border-radius: var(--r-sm);
  padding: 4px 7px;
  white-space: nowrap;
}

.part-cta {
  justify-self: start;
  font: 600 11px/1 var(--f-body);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--c-accent-text);
  background: none;
  border: var(--bw-1) solid var(--c-border-strong);
  border-radius: var(--r-sm);
  padding: 0 var(--s-3);
  min-height: 36px;
  cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease-out), background var(--dur-fast) var(--ease-out);
}
.part-cta:hover { border-color: var(--c-accent); background: var(--c-surface-2); }

@media (min-width: 768px) {
  .part-row {
    grid-template-columns: minmax(0, 1fr) minmax(0, 15rem) auto;
    align-items: center;
    gap: var(--s-4);
  }
  .part-main { min-width: 0; }
  .part-meta { min-width: 0; }
  .part-cta { justify-self: end; }
}

/* ---- Sub-group accordion ----
   Ships OPEN in the HTML and is collapsed by JS below 768px. That ordering is
   the point: crawlers and anyone without JS get the complete parts list, and
   the phone gets a shorter page. Degradation runs toward more content, never
   less — which matters because most AI crawlers do not execute JavaScript. */
.part-group {
  border: var(--bw-1) solid var(--c-border);
  border-radius: var(--r-md);
  background: var(--c-surface);
  margin-bottom: var(--s-3);
  overflow: hidden;
}
.part-group > summary {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-3) var(--s-4);
  min-height: 44px;
  cursor: pointer;
  list-style: none;
  font: 600 14px/1.3 var(--f-display);
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--c-text);
  background: var(--c-surface-2);
}
.part-group > summary::-webkit-details-marker { display: none; }
.part-group > summary::marker { content: ""; }
.part-group > summary:hover { color: var(--c-accent-text); }
.part-group > summary:focus-visible {
  outline: var(--bw-2) solid var(--c-accent);
  outline-offset: -2px;
}
.part-group-count {
  margin-left: auto;
  font: 500 11px/1 var(--f-mono);
  letter-spacing: 0.08em;
  color: var(--c-text-subtle);
  white-space: nowrap;
}
.part-group-chevron {
  width: 10px; height: 10px;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: rotate(45deg);
  transition: transform var(--dur-fast) var(--ease-out);
  flex-shrink: 0;
}
.part-group[open] > summary .part-group-chevron { transform: rotate(-135deg); }
.part-group-body { padding: 0 var(--s-4) var(--s-2); }
/* Keeps a 105-row page cheap to render on a weak device. */
.part-group:not([open]) { content-visibility: auto; }

.part-group-fits {
  font: 400 13px/1.5 var(--f-body);
  color: var(--c-text-muted);
  padding: var(--s-3) 0 0;
}
.part-group-fits a { color: var(--c-accent-text); text-decoration: underline; text-underline-offset: 2px; }

.group-controls {
  display: flex; gap: var(--s-3); align-items: center;
  margin-bottom: var(--s-3);
}
.group-toggle-all {
  font: 500 12px/1 var(--f-body);
  letter-spacing: 0.01em;
  color: var(--c-text-muted);
  background: none; border: 0; padding: 8px 0; cursor: pointer;
}
.group-toggle-all:hover { color: var(--c-accent-text); }

@media (prefers-reduced-motion: reduce) {
  .part-group-chevron, .part-cta { transition: none; }
}

/* ---- Machine filter chips (reuse .tag-list / a.tag) ---- */
.machine-filter { margin: var(--s-4) 0 var(--s-5); }
.machine-filter-label {
  display: block;
  font: 600 12px/1 var(--f-body);
  letter-spacing: 0.01em;
  color: var(--c-text-subtle);
  margin-bottom: var(--s-2);
}

/* ---- Catalogue freshness stamp ---- */
.catalog-stamp {
  font: 500 11px/1.4 var(--f-mono);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--c-text-subtle);
  padding-top: var(--s-4);
  border-top: var(--bw-1) solid var(--c-border);
  margin-top: var(--s-6);
}

/* ---- Tile without a photograph ----
   A designed state, not a missing asset: a plate with the category code set
   large where the photo would be. Mirrors how the brand marquee already
   degrades a missing logo to a wordmark. Applies to machine AND category tiles
   — without it a tile with no image renders as a bare white rectangle that
   reads as broken. Drop a photo in and this stops applying automatically.

   This used to also re-light every label colour, because tiles were white text
   on a dark scrim and this state was the one light exception. Now that tiles
   are light by default those overrides are all no-ops, so only the plate and
   the watermark remain. No tile uses this today — all 22 have photos. */
.tile[data-nophoto] .tile-bg {
  background: linear-gradient(150deg, var(--c-surface-2) 0%, #E3E7EA 100%);
}
/* Sits in the photo area, above the label bar. */
.tile[data-nophoto]::after {
  content: attr(data-nophoto);
  position: absolute;
  right: var(--s-4); top: 34%;
  transform: translateY(-50%);
  font: 700 clamp(52px, 11vw, 104px)/1 var(--f-display);
  color: rgba(20,23,27,0.09);
  letter-spacing: -0.03em;
  pointer-events: none;
}

/* ---- Products submenu ----
   A disclosure, deliberately NOT role="menu". That role declares
   application-menu semantics and obliges arrow-key navigation; this is a list
   of links, and role="menu" is the most commonly botched ARIA pattern on the
   web. A <button aria-expanded> plus plain <a>s is simpler and correct.

   The "Products" text stays a real link with a separate chevron button beside
   it, so the landing page never becomes unreachable — the classic trap when a
   top-level nav item is converted into a menu trigger. */
.nav-item { position: relative; display: inline-flex; align-items: center; gap: 2px; }

.nav-submenu-toggle {
  display: inline-flex; align-items: center; justify-content: center;
  width: 26px; height: 26px; padding: 0;
  color: var(--c-text-muted);
  border-radius: var(--r-sm);
}
.nav-submenu-toggle::after {
  content: "";
  width: 7px; height: 7px;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: translateY(-2px) rotate(45deg);
  transition: transform var(--dur-fast) var(--ease-out);
}
.nav-submenu-toggle[aria-expanded="true"]::after { transform: translateY(1px) rotate(-135deg); }
.nav-submenu-toggle:hover { color: var(--c-text); }

.submenu {
  display: none;
  position: absolute;
  top: calc(100% + 14px);
  left: 50%;
  transform: translateX(-50%);
  z-index: 120;
  min-width: 540px;
  background: var(--c-surface);
  border: var(--bw-1) solid var(--c-border);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-3);
  padding: var(--s-5);
}
.submenu[data-open="true"] { display: block; }
.submenu-inner { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s-6); }
.submenu-head {
  font: 600 12px/1 var(--f-body);
  letter-spacing: 0.01em;
  color: var(--c-text-subtle);
  margin-bottom: var(--s-3);
}
.submenu ul { display: flex; flex-direction: column; gap: 1px; }
.submenu li a {
  display: block;
  padding: 7px var(--s-2);
  font: 400 13px/1.3 var(--f-body);
  color: var(--c-text-muted);
  border-radius: var(--r-sm);
}
.submenu li a:hover { background: var(--c-surface-2); color: var(--c-text); }
.submenu-count { color: var(--c-text-subtle); font: 400 11px/1 var(--f-mono); margin-left: 4px; }
.submenu-all {
  display: block;
  margin-top: var(--s-4);
  padding-top: var(--s-3);
  border-top: var(--bw-1) solid var(--c-border);
  font: 600 11px/1 var(--f-body);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--c-accent-text);
}
.submenu-all:hover { color: var(--c-accent-hover); }

/* Mobile sheet: a nested <details>, reusing the accordion built for the
   catalog sub-groups rather than inventing a second disclosure. */
/* Match the sibling top-level links exactly — the summary is a peer of
   Home/Export/About, so it has to read as one. */
.mobile-nav .nav-details > summary {
  display: flex; align-items: center; gap: var(--s-2);
  list-style: none; cursor: pointer;
  padding: var(--s-5) 0;
  font: 600 22px/1 var(--f-display);
  text-transform: uppercase;
  letter-spacing: 0.02em;
  color: var(--c-text);
  border-bottom: var(--bw-1) solid var(--c-border);
}
.mobile-nav .nav-details > summary::-webkit-details-marker { display: none; }
.mobile-nav .nav-details > summary::marker { content: ""; }
.mobile-nav .nav-details > summary::after {
  content: "";
  width: 8px; height: 8px;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: rotate(45deg);
  margin-left: auto;
  transition: transform var(--dur-fast) var(--ease-out);
}
.mobile-nav .nav-details[open] > summary::after { transform: rotate(-135deg); }
.mobile-nav .nav-sub {
  padding: var(--s-2) 0 var(--s-3) var(--s-4);
  display: flex; flex-direction: column; gap: 2px;
}
.mobile-nav .nav-sub-head {
  font: 600 12px/1 var(--f-body);
  letter-spacing: 0.01em;
  color: var(--c-text-subtle);
  margin: var(--s-3) 0 var(--s-1);
}
/* Sub-links sit a level down, so they drop to body type and lose the border
   the top-level links carry — otherwise the hierarchy reads flat. */
.mobile-nav .nav-sub a {
  font: 400 15px/1.3 var(--f-body);
  text-transform: none;
  letter-spacing: 0;
  padding: 10px 0;
  color: var(--c-text-muted);
  border-bottom: 0;
}
.mobile-nav .nav-sub a:hover { color: var(--c-text); }

@media (prefers-reduced-motion: reduce) {
  .nav-submenu-toggle::after,
  .mobile-nav .nav-details > summary::after { transition: none; }
}
