
/* ============================================================
   §NA · NO BARE COLOURED ACCENT LINES  (permanent rule)
   ------------------------------------------------------------
   Abhay, 2026-08-18: a bare vertical or horizontal coloured bar
   on a box, card, tile, callout or media frame instantly reads
   as an AI-generated layout. It is banned SITEWIDE.

   Structure now comes from spacing, type hierarchy, background
   and real content — never a decorative colour bar.

   ⚠ THIS SUPERSEDES the DESIGN LANGUAGE SPEC v1 "engineered
   framing" signature (a 1-2px brand rule on ONE edge of every
   photograph). That device is retired by this rule. The frame,
   its radius, its tile gradient and its tracked mono fact
   caption all REMAIN — only the coloured edge bar is gone.

   DELIBERATELY KEPT, because none of these is an accent bar:
     · 6x6 brand squares used as LIST BULLETS (a marker)
     · the dimension-annotation / technical-drawing linework
       (.mech-dim, the caliper) — that is draughting, not decor
     · the explorer hot-dot (a dot)
   ============================================================ */
/* ==========================================================================
   Shaheen Arabia — enterprise INNER-PAGE component library (Phase 1)
   Light-canvas dominant, shared across all non-home pages. Tokens + base type
   from app.css. Front page uses home.css instead.
   ========================================================================== */

/* eyebrow: clean tracked caps in brand green — NO leading rule/dash (retired 2026-07-28).
   The base .eyebrow treatment in app.css is the single sitewide definition. */

/* breadcrumb */
.crumb{display:flex;align-items:center;gap:9px;font-size:.82rem;color:var(--tx-l-dim);margin-bottom:20px}
.crumb a{color:var(--tx-l-dim)}
.crumb a:hover{color:var(--brand-ink)}
.crumb span[aria-hidden]{opacity:.5}
.crumb [aria-current]{color:var(--tx-l)}

/* ---------- page hero ---------- */
.p-hero{padding-top:var(--hdr-h);border-bottom:1px solid var(--hair-l)}
.p-hero-grid{display:grid;grid-template-columns:1fr;gap:clamp(28px,5vw,64px);align-items:center;
  padding-block:clamp(34px,3.6vw,52px)}   /* ATF standard 2026-08-17, was 48-96 */
.p-hero.has-img .p-hero-grid{grid-template-columns:1.15fr .85fr}
.p-hero-copy{min-width:0;max-width:60ch}
.p-hero.has-img .p-hero-copy{max-width:none}
.p-hero h1{margin-top:18px;max-width:20ch;text-wrap:balance}
/* The 20ch cap is NOT what was breaking these headlines — it computes to ~870px
   against a column of ~571px, so it never bound. The cause was the global h1
   scale (clamp 40-62px), which was sized for a full-width hero measure, being
   used inside a ~40%-width column: 62px type in 571px is ~13 characters a line,
   so a 66-character headline fell into five lines with single words on three of
   them. Where the hero has a second column the measure is roughly half, so the
   display step is reduced to match it. Full-width heroes are untouched. */
/* The narrow-column hero H1. Retuned with the global scale: it must stay AT OR
   BELOW the global cap, never above it, or the override reintroduces the
   oversized heading the cap exists to prevent. */
.p-hero.has-img h1{font-size:clamp(1.75rem,2.7vw,2.5rem)}    /* 28–40 — the min was 27px, one under the standard's 28px mobile floor; caught by measuring at 390, not by reading it */
.p-hero h1 em{font-style:normal;color:var(--brand-ink)}
.p-hero .lead{margin-top:20px;max-width:60ch}

.p-hero-fig{position:relative;display:grid;place-items:center;min-height:clamp(280px,36vw,460px)}
.p-hero-fig::before{content:"";position:absolute;inset:0;
  background:radial-gradient(58% 58% at 52% 46%,var(--off) 0%,#FAFAF9 55%,rgba(250,250,249,0) 78%)}
.p-hero-shot{position:relative;width:fit-content;max-width:100%}
.p-hero-fig img{position:relative;max-height:clamp(280px,36vw,460px);width:auto;object-fit:contain;border-radius:var(--r-sm)}
.p-hero-tag{position:absolute;right:-8px;bottom:6px;z-index:2;background:var(--white);
  border:1px solid var(--hair-l);padding:10px 14px;max-width:230px;
  font-size:.78rem;color:var(--tx-l-dim);line-height:1.45}
.p-hero-tag b{display:block;font-family:var(--f-dsp);font-weight:600;font-size:.9rem;color:var(--tx-l);letter-spacing:-.01em;margin-bottom:2px}

/* ---------- generic section rhythm ---------- */
.p-block{padding-block:var(--sp);border-top:1px solid var(--hair-l)}
/* The rhythm token was never the problem — it resolves to the intended 77.76px
   on every one of these sections. The dead band is the BOUNDARY: two adjacent
   blocks each pay a full step, so 156px of pure padding stacks up, and the
   section head then adds another ~54px of accent before the heading is even
   reached. A boundary between two blocks does not need two full steps; the
   first block after the hero still gets one, because that one is an entrance. */
.p-block + .p-block{padding-top:var(--sp-sm)}
.p-block.alt{background:var(--off)}
.p-block.dark{background:var(--ink);color:var(--tx-d);border-top-color:var(--hair-d)}
.p-block.dark .lead,.p-block.dark p{color:var(--tx-d-dim)}
.p-head{max-width:64ch;margin-bottom:clamp(26px,3.2vw,38px)}
.p-head h2{margin-top:16px;max-width:22ch}
.p-head .lead{margin-top:16px}

/* two-column intro (heading | prose) */
.p-split{display:grid;grid-template-columns:.85fr 1.15fr;gap:clamp(28px,5vw,64px);align-items:start}
.p-split h2{max-width:16ch}
.p-split .prose{display:flex;flex-direction:column;gap:16px}
.p-split .prose p{color:var(--tx-l-dim);max-width:64ch}
.p-block.dark .p-split .prose p{color:var(--tx-d-dim)}

/* prose (privacy, long text) */
.p-prose{max-width:72ch}
/* When .p-prose is put ON the .wrap itself, its 72ch cap replaces the container
   width and .wrap's margin-inline:auto then centres the SHRUNKEN box — so the
   block landed at x=357 while every other section on the page sat on the
   container axis at x=144. Restore the container box and cap the reading
   measure on the children instead, which keeps the 72ch measure exactly as it
   was and puts the block back on the page axis.
   Scoped to .wrap.p-prose on purpose: privacy-policy nests it as
   .wrap > .p-prose, which was never off-axis and is deliberately untouched. */
.wrap.p-prose{max-width:var(--container)}
.wrap.p-prose > *{max-width:72ch}
.p-prose h2{font-size:clamp(1.4rem,2vw,1.7rem);margin-top:40px;margin-bottom:12px}
.p-prose h2:first-child{margin-top:0}
.p-prose h3{font-size:1.1rem;margin-top:26px;margin-bottom:8px}
.p-prose p,.p-prose li{color:var(--tx-l-dim);line-height:1.7}
.p-prose p{margin-bottom:14px;max-width:72ch}
.p-prose ul{margin:0 0 16px;padding-left:0}
.p-prose li{position:relative;padding-left:22px;margin-bottom:8px}
.p-prose li::before{content:"";position:absolute;left:2px;top:11px;width:6px;height:6px;background:var(--brand-graphic);border-radius:50%}
.p-prose a{color:var(--brand-ink);text-decoration:underline;text-underline-offset:2px}

/* ---------- feature grid — borderless tinted cards, soft radius, lift on hover ---------- */
.p-grid{display:grid;gap:clamp(14px,1.6vw,22px)}
.p-grid.c2{grid-template-columns:repeat(2,1fr)}
.p-grid.c3{grid-template-columns:repeat(3,1fr)}
.p-grid.c4{grid-template-columns:repeat(4,1fr)}
.p-cell{display:flex;flex-direction:column;background:var(--tint);border-radius:var(--r);
  padding:clamp(24px,2.4vw,32px);transition:transform .3s cubic-bezier(.2,.7,.3,1),box-shadow .3s ease}
.p-block.alt .p-cell{background:var(--white)}
.p-block.dark .p-cell{background:var(--ink-2)}
.p-cell:hover{transform:translateY(-3px);box-shadow:var(--shadow)}
.p-cell .n{font-family:var(--f-body);font-weight:600;font-size:.68rem;letter-spacing:.13em;text-transform:uppercase;
  color:var(--brand-ink);font-variant-numeric:tabular-nums}
.p-cell h3{margin-top:12px;font-size:clamp(1.15rem,1.5vw,1.38rem);max-width:20ch}
.p-cell p{margin-top:10px;font-size:.92rem;line-height:1.58;color:var(--tx-l-dim)}
.p-block.dark .p-cell h3{color:var(--tx-d)}
.p-block.dark .p-cell p{color:var(--tx-d-dim)}

/* ---------- sector cards — image-led, so an industries page shows industry ---------- */
.sector{flex-direction:row;gap:clamp(18px,2vw,26px);align-items:center;padding:clamp(16px,1.6vw,20px)}
.sector-media{flex:none;width:clamp(120px,15vw,196px);aspect-ratio:4/3;border-radius:var(--r-sm);
  overflow:hidden;background:#0f1114;position:relative}
.sector-media::after{display:none}
/* No scrim here: nothing is overlaid on these thumbnails, and the provisional
   renders are already dark — dimming them turns a 168px tile into a black box. */
.sector-media img{width:100%;height:100%;object-fit:cover;opacity:1;transition:transform .5s ease}
.sector:hover .sector-media img{transform:scale(1.05)}
.sector-say{min-width:0}
.sector h3{margin-top:0}
@media (max-width:560px){
  .sector{flex-direction:column;align-items:flex-start;gap:14px}
  .sector-media{width:100%;aspect-ratio:16/9}
}

/* ---------- simple tag/chip list (services, families, industries) ---------- */
.p-chips{display:flex;flex-wrap:wrap;gap:10px}
.p-chips li{font-family:var(--f-body);font-weight:500;font-size:.95rem;color:var(--tx-l);
  background:var(--tint);border-radius:var(--r-sm);padding:12px 20px;transition:background .2s,color .2s}
.p-chips li:hover{background:var(--tint-2);color:var(--brand-ink)}

/* ---------- catalogue list (dense component lists) ---------- */
.p-list{columns:2;column-gap:48px}
.p-list li{break-inside:avoid;padding:9px 0 9px 20px;position:relative;border-bottom:1px solid var(--hair-l);color:var(--tx-l);font-size:.94rem}
.p-list li::before{content:"";position:absolute;left:0;top:16px;width:6px;height:6px;background:var(--brand-graphic);border-radius:50%}

/* ---------- spec / materials table ---------- */
.p-table{width:100%;border-collapse:collapse;font-size:.94rem}
.p-table caption{text-align:left;font-family:var(--f-body);font-weight:600;font-size:.72rem;letter-spacing:.14em;
  text-transform:uppercase;color:var(--brand-ink);padding-bottom:18px}
.p-table th,.p-table td{text-align:left;padding:15px 18px;border-bottom:1px solid var(--hair-l);vertical-align:top}
.p-table thead th{font-family:var(--f-body);font-weight:600;font-size:.74rem;letter-spacing:.1em;text-transform:uppercase;color:var(--tx-l-dim)}
.p-table tbody th{font-family:var(--f-dsp);font-weight:600;color:var(--tx-l);white-space:nowrap}
.p-table td{color:var(--tx-l-dim)}
.p-table-wrap{overflow-x:auto}

/* ---------- labelled reason/value list (dark or light) ---------- */
.p-defs{display:flex;flex-direction:column}
.p-defs li{padding:16px 0;border-top:1px solid var(--hair-l);display:grid;grid-template-columns:minmax(180px,1fr) 2fr;gap:20px;align-items:baseline}
.p-defs li:first-child{border-top:0;padding-top:0}
.p-defs b{font-family:var(--f-dsp);font-weight:600;font-size:1rem;letter-spacing:-.01em;color:var(--tx-l)}
.p-defs span{color:var(--tx-l-dim);font-size:.94rem;line-height:1.55}
.p-block.dark .p-defs li{border-top-color:var(--hair-d)}
.p-block.dark .p-defs b{color:var(--tx-d)}
.p-block.dark .p-defs span{color:var(--tx-d-dim)}

/* ---------- stats row (airy — whitespace, not dividers) ---------- */
.p-stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:clamp(24px,3vw,44px)}
.p-stat{padding-top:18px;border-top:1px solid var(--hair-l)}
.p-stat .num{font-size:clamp(1.8rem,2.6vw,2.4rem);display:block;color:var(--tx-l)}
.p-stat .num .accent{color:var(--brand-ink);font-size:.5em;margin-left:2px}
.p-stat p{margin-top:10px;font-size:.85rem;color:var(--tx-l-dim);line-height:1.45;max-width:24ch}
.p-block.dark .p-stat{border-top-color:var(--hair-d)}
.p-block.dark .p-stat .num{color:var(--tx-d)}
.p-block.dark .p-stat p{color:var(--tx-d-dim)}

/* ---------- callout / warranty / note ---------- */
.p-callout{padding:20px 24px;background:var(--off);border-radius:var(--r);max-width:70ch}
.p-callout .big{font-family:var(--f-dsp);font-weight:600;font-size:clamp(1.4rem,2vw,1.8rem);letter-spacing:-.02em;color:var(--tx-l)}
.p-callout p{margin-top:8px;color:var(--tx-l-dim)}
/* No decorative left rule. The client rejected the standalone vertical accent
   three times on the homepage; those fixes were homepage-scoped, so it survived
   here and marked ordinary group-description copy as if it were an aside.
   The note reads as a note through size and dimmed colour, which it already had. */
.p-note{font-size:.82rem;color:var(--tx-l-dim);line-height:1.5;margin-top:16px}
.p-block.dark .p-callout{background:var(--ink-2)}
.p-block.dark .p-callout .big{color:var(--tx-d)}
.p-block.dark .p-callout p,.p-block.dark .p-note{color:var(--tx-d-dim)}

/* ---------- downloads ---------- */
.p-downloads{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:14px}
/* Inside a p-split prose column there is only room for two 240px tracks, so a
   third card orphaned on its own row. Stacked, all three get the full column
   width and the longest label ("Anti-Bribery & Anti-Harassment") stops wrapping
   to three lines. Scoped to this context so the full-width use on
   /capabilities/ keeps its side-by-side pair. */
/* Stacking all three fixed the orphan but made this column 416px against a
   124px heading — the largest p-split imbalance on the site, and one I caused.
   Two tracks with the odd last card spanning the row fixes both: no orphan, and
   the column drops to two rows. Self-correcting — with an even count nothing
   spans. Same device as the RFQ form's full-width last field. */
.p-split .prose .p-downloads{grid-template-columns:repeat(2,1fr)}
.p-split .prose .p-downloads > :last-child:nth-child(odd){grid-column:1/-1}
@media (max-width:640px){.p-split .prose .p-downloads{grid-template-columns:1fr}}

/* ==========================================================================
   FIGBLOCK — the ONE new layout added this sprint (2026-07-28).
   Breaks the 19x .p-split monotony: a full-bleed-within-the-grid engineered
   frame on one side, and on the other the capability's approved one-line
   proposition set as an ISOLATED STATEMENT above a short fact list.
   Deliberately ONE new device, not two — a second would just become a new
   template tell. Used once per capability page, where monotony was worst.
   Alternates side by position via .figblock.flip.
   ========================================================================== */
.figblock{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);
  gap:clamp(28px,4.5vw,72px);align-items:center}
.figblock.flip .figblock-fig{order:2}
.figblock .eframe{aspect-ratio:4/3}
.figblock-say .eyebrow{display:inline-block}
.figblock-prop{font-family:var(--f-dsp);font-weight:600;letter-spacing:-.022em;
  font-size:clamp(1.5rem,2.6vw,2.2rem);line-height:1.12;color:var(--tx-l);
  max-width:16ch;margin-top:14px}
.figblock-prop em{font-style:normal;color:var(--brand-ink)}
.figblock-say > p{margin-top:16px;color:var(--tx-l-dim);max-width:48ch}
.figblock-facts{margin-top:22px;display:flex;flex-direction:column}
/* fixed label column so every row's value shares a left edge */
.figblock-facts li{padding:13px 0;border-top:1px solid var(--hair-l);
  display:grid;grid-template-columns:112px minmax(0,1fr);gap:14px;align-items:baseline}
@media (max-width:560px){.figblock-facts li{grid-template-columns:1fr;gap:3px}}
.figblock-facts b{font-family:var(--f-dsp);font-weight:600;font-size:.95rem;
  color:var(--brand-ink);white-space:nowrap}
.figblock-facts span{font-size:.9rem;line-height:1.5;color:var(--tx-l-dim)}
.p-block.dark .figblock-prop{color:var(--tx-d)}
.p-block.dark .figblock-prop em{color:var(--brand-graphic)}
.p-block.dark .figblock-say > p,.p-block.dark .figblock-facts span{color:var(--tx-d-dim)}
.p-block.dark .figblock-facts li{border-top-color:var(--hair-d)}
.p-block.dark .figblock-facts b{color:var(--brand-graphic)}
@media (max-width:860px){
  .figblock{grid-template-columns:1fr;gap:26px}
  .figblock.flip .figblock-fig{order:0}
}

/* ---------- process sequence — numbered steps on rules, not cards ---------- */
.p-steps{display:flex;flex-direction:column;counter-reset:step}
.p-step{display:grid;grid-template-columns:auto minmax(0,1fr);gap:clamp(18px,3vw,44px);
  align-items:start;padding:clamp(20px,2.4vw,30px) 0;border-top:1px solid var(--hair-l)}
.p-step:last-child{border-bottom:1px solid var(--hair-l)}
.p-step-n{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
  font-size:.72rem;letter-spacing:.12em;color:var(--brand-ink);padding-top:6px}
.p-step h3{font-family:var(--f-dsp);font-weight:600;letter-spacing:-.015em;
  font-size:clamp(1.1rem,1.5vw,1.35rem);color:var(--tx-l)}
.p-step p{margin-top:8px;font-size:.94rem;line-height:1.6;color:var(--tx-l-dim);max-width:62ch}
.p-block.dark .p-step{border-top-color:var(--hair-d)}
.p-block.dark .p-step:last-child{border-bottom-color:var(--hair-d)}
.p-block.dark .p-step h3{color:var(--tx-d)}
.p-block.dark .p-step p{color:var(--tx-d-dim)}
.p-block.dark .p-step-n{color:var(--brand-graphic)}

/* ---------- RFQ / forms ---------- */
/* ONE grid for the whole form, top to bottom.
   Before: .rrow was its own 2-track grid with a 34px gap and .rrow3 its own
   3-track grid with a 30px gap, both inside a 760px form — so a field was 363px
   in "Your details" and 233px in "Specification", on gridlines that did not
   correspond. The width changed halfway down the form for no reason a user
   could see. Now every row shares one 6-column grid and one gap, so a field is
   either half-width or full-width and nothing else, and every edge lands on the
   same line. .rrow is display:contents, so the rows keep their grouping in the
   markup while their fields participate in the single parent grid. */
.p-form{display:grid;grid-template-columns:repeat(6,1fr);gap:30px;max-width:860px}
.p-form > *{grid-column:1/-1}
.p-fieldset{border:0;padding:0;margin:0;display:grid;grid-template-columns:repeat(6,1fr);gap:30px}
.p-fieldset > *{grid-column:1/-1}
.p-form .rrow{display:contents}
.p-form .rrow > .field{grid-column:span 3}
.p-form .field.is-wide{grid-column:1/-1}
.p-fieldset > legend{font-family:var(--f-body);font-weight:600;font-size:.72rem;letter-spacing:.14em;
  text-transform:uppercase;color:var(--brand-ink);margin-bottom:6px;padding:0}
.p-file{display:flex;align-items:center;gap:14px;flex-wrap:wrap;border:1px dashed var(--hair-l);border-radius:var(--r);padding:16px}
.p-file:focus-within{border-color:var(--brand-graphic)}
.p-file input[type=file]{font:inherit;font-size:.9rem;color:var(--tx-l-dim);border:0}
.p-file .hint{font-size:.78rem;color:var(--tx-l-dim)}
.p-consent{display:flex;gap:12px;align-items:flex-start;font-size:.9rem;color:var(--tx-l-dim);max-width:60ch}
.p-consent input{margin-top:3px;width:18px;height:18px;accent-color:var(--brand-ink);flex:none}
.p-consent a{color:var(--brand-ink);text-decoration:underline}
.p-formbanner{padding:14px 18px;border-radius:var(--r);font-size:.92rem;border-left:2px solid}
.p-formbanner.err{background:rgba(4,105,56,.08);border-color:var(--brand-ink);color:var(--tx-l)}
/* .rrow is display:contents, so app.css's `.rrow{grid-template-columns:1fr}`
   breakpoint can no longer stack these — the row has no box to restyle. The
   fields have to be stacked on the parent grid instead. */
@media (max-width:640px){
  .p-form .rrow > .field{grid-column:1/-1}
}

/* ---------- gallery — engineered framing (brand edge alternates, mono fact caption) ---------- */
.p-gallery{display:grid;grid-template-columns:repeat(auto-fill,minmax(230px,1fr));gap:clamp(14px,1.6vw,22px)}
.p-gallery .eframe{aspect-ratio:4/3}

/* ---------- responsive ---------- */
@media (max-width:1024px){ .p-grid.c4{grid-template-columns:repeat(2,1fr)} }
@media (max-width:860px){
  .p-hero.has-img .p-hero-grid{grid-template-columns:1fr}
  .p-split{grid-template-columns:1fr;gap:22px}
  .p-grid.c3{grid-template-columns:1fr 1fr}
  .p-list{columns:1}
}
@media (max-width:560px){
  .p-grid.c2,.p-grid.c3,.p-grid.c4{grid-template-columns:1fr}
  .p-defs li{grid-template-columns:1fr;gap:4px}
  .p-defs b{white-space:normal}
  .p-stats{grid-template-columns:1fr 1fr}
}

/* map embed sits on a light scene here — drop the dark-theme invert from app.css */
.scene-light .map-bleed iframe{filter:grayscale(.25) contrast(1)}
.map-bleed{border-radius:0}

/* ---------- FAQ accordion (native <details>, no JS) ---------- */
/* The 80ch cap belonged to the ANSWER, not to the accordion. Applied to the
   list it stopped every bar at ~870px inside a 1152px section, so ~430px sat
   empty beside every question on all four sections — the whole right third of
   a page that is nothing but accordions. The bars are controls and should use
   the column; the answer paragraph keeps the reading measure. */
.faq-list{display:flex;flex-direction:column;gap:10px}
.faq-a p{max-width:80ch}
.faq-item{background:var(--tint);border-radius:var(--r);overflow:hidden;transition:background .2s}
.p-block.alt .faq-item{background:var(--white)}
.faq-item summary{list-style:none;cursor:pointer;display:flex;align-items:flex-start;justify-content:space-between;
  gap:20px;padding:clamp(18px,2vw,24px);font-family:var(--f-dsp);font-weight:600;letter-spacing:-.01em;
  font-size:clamp(1rem,1.2vw,1.14rem);color:var(--tx-l);line-height:1.4}
.faq-item summary::-webkit-details-marker{display:none}
.faq-item summary i{flex:none;position:relative;width:16px;height:16px;margin-top:3px}
.faq-item summary i::before,.faq-item summary i::after{content:"";position:absolute;background:var(--brand-ink);
  transition:transform .25s ease,opacity .25s ease}
.faq-item summary i::before{left:0;top:7px;width:16px;height:2px}
.faq-item summary i::after{left:7px;top:0;width:2px;height:16px}
.faq-item[open] summary i::after{transform:rotate(90deg);opacity:0}
.faq-item[open] summary{color:var(--brand-ink)}
.faq-a{padding:0 clamp(18px,2vw,24px) clamp(20px,2.2vw,26px)}
.faq-a p{color:var(--tx-l-dim);line-height:1.65;max-width:70ch;margin:0}
.faq-item:hover{background:var(--tint-2)}
.p-block.alt .faq-item:hover{background:var(--tint)}

/* ---------- blog: listing + single ---------- */
.blog-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:clamp(18px,2.2vw,28px)}
.bcard{display:flex;flex-direction:column;background:var(--tint);border-radius:var(--r-lg);overflow:hidden;
  transition:transform .3s cubic-bezier(.2,.7,.3,1),box-shadow .3s ease}
.bcard:hover{transform:translateY(-4px);box-shadow:var(--shadow-lg)}
.bcard-media{position:relative;aspect-ratio:16/10;background:radial-gradient(70% 70% at 50% 45%,#20242b 0%,#0f1114 78%);overflow:hidden}
.bcard-media::after{display:none}
.bcard-media img{width:100%;height:100%;object-fit:cover}
.bcard-body{padding:clamp(22px,2.2vw,30px);display:flex;flex-direction:column;gap:10px;flex:1}
.bcard-meta{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.68rem;letter-spacing:.1em;
  text-transform:uppercase;color:var(--brand-ink)}
.bcard h3{font-family:var(--f-dsp);font-weight:600;letter-spacing:-.018em;font-size:clamp(1.1rem,1.4vw,1.32rem);
  color:var(--tx-l);line-height:1.3}
.bcard p{font-size:.92rem;line-height:1.55;color:var(--tx-l-dim);margin:0}
.bcard .rd{margin-top:auto;padding-top:8px;display:inline-flex;align-items:center;gap:8px;font-size:.86rem;font-weight:600;color:var(--tx-l)}
.bcard:hover .rd{color:var(--brand-ink)}

.blog-empty{background:var(--tint);border-radius:var(--r-lg);padding:clamp(36px,5vw,64px);text-align:center;max-width:62ch;margin-inline:auto}
.blog-empty h3{font-family:var(--f-dsp);font-weight:600;font-size:clamp(1.2rem,1.6vw,1.5rem);color:var(--tx-l)}
.blog-empty p{margin-top:12px;color:var(--tx-l-dim);margin-inline:auto}

.bpager{display:flex;gap:10px;justify-content:center;margin-top:clamp(32px,4vw,52px);flex-wrap:wrap}
.bpager .page-numbers{display:inline-flex;align-items:center;justify-content:center;min-width:44px;height:44px;
  padding:0 14px;border-radius:var(--r-sm);background:var(--tint);color:var(--tx-l);font-weight:600;font-size:.92rem;
  transition:background .2s,color .2s}
.bpager .page-numbers:hover{background:var(--tint-2);color:var(--brand-ink)}
.bpager .page-numbers.current{background:var(--brand-ink);color:#fff}

/* single */
.bsingle{max-width:74ch}
.bsingle-meta{display:flex;flex-wrap:wrap;gap:8px 18px;align-items:center;margin-top:18px;
  font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.7rem;letter-spacing:.1em;
  text-transform:uppercase;color:var(--tx-l-dim)}
.bsingle-meta .sep{width:5px;height:5px;background:var(--brand-graphic);flex:none}
.bbody{margin-top:clamp(28px,3.5vw,44px)}
.bbody h2{font-family:var(--f-dsp);font-weight:600;font-size:clamp(1.4rem,2vw,1.75rem);margin:38px 0 12px;color:var(--tx-l)}
.bbody h3{font-family:var(--f-dsp);font-weight:600;font-size:1.15rem;margin:26px 0 8px;color:var(--tx-l)}
.bbody p,.bbody li{color:var(--tx-l-dim);line-height:1.75}
.bbody p{margin-bottom:16px}
.bbody ul,.bbody ol{margin:0 0 18px;padding-left:0}
.bbody li{position:relative;padding-left:22px;margin-bottom:8px;list-style:none}
.bbody li::before{content:"";position:absolute;left:2px;top:12px;width:6px;height:6px;background:var(--brand-graphic);border-radius:50%}
.bbody blockquote{margin:26px 0;padding:20px 24px;background:var(--tint);border-radius:var(--r)}
.bbody blockquote p{margin:0;color:var(--tx-l);font-family:var(--f-dsp);font-weight:600;font-size:1.08rem;line-height:1.5}
.bbody img{border-radius:var(--r);margin:22px 0}
.bbody a{color:var(--brand-ink);text-decoration:underline;text-underline-offset:2px}
.bauthor{margin-top:clamp(32px,4vw,48px);padding:22px;background:var(--tint);border-radius:var(--r);display:flex;gap:16px;align-items:flex-start}
.bauthor b{font-family:var(--f-dsp);font-weight:600;color:var(--tx-l);display:block}
.bauthor span{font-size:.9rem;color:var(--tx-l-dim)}

@media (max-width:960px){ .blog-grid{grid-template-columns:1fr 1fr} }
@media (max-width:600px){ .blog-grid{grid-template-columns:1fr} }

/* ==========================================================================
   PLACEHOLDER SURFACE (2026-07-29) — ONE component for every missing visual.
   Dark surface, subtle brand-green accent, centred mark, self-documenting
   label, INDICATIVE VISUAL corner tag. Never a broken-image icon, never
   stretched stock, never a blank box. All markers switch off together via
   SACOM_PLACEHOLDER in inc/blocks.php.
   ========================================================================== */
/* MOVED to app.css 2026-07-31 — see the note there. This is a SHARED render
   helper and must not live in a page-scoped stylesheet. */

/* ---------- category filter (products + gallery) ---------- */
/* A wrapping flex row cannot be told where to break, so seven pills of unequal
   width left one orphan alone on row 2 at 1440 and stacked into five ragged
   rows at 390. A no-wrap rail has no break to get wrong at any width: the row
   is always one row. It fits outright at 1440 and scrolls where it cannot,
   which is the standard behaviour for a mobile filter bar rather than a
   degraded state. Buttons stay real buttons, so focus scrolls them into view.
   The negative margin lets the rail bleed to the wrap edge while keeping the
   first and last pill on the text axis. */
.cat-filter{display:flex;flex-wrap:nowrap;gap:6px;margin-bottom:clamp(26px,3vw,40px);
  overflow-x:auto;overflow-y:hidden;scrollbar-width:none;-webkit-overflow-scrolling:touch;
  padding:4px 0;margin-inline:-4px;padding-inline:4px;scroll-padding-inline:4px}
.cat-filter::-webkit-scrollbar{display:none}
/* Sized so the longest set (gallery's seven) clears 1152 outright with room to
   spare — a rail that scrolls at desktop would just look like a clipped pill. */
.cat-btn{flex:0 0 auto;white-space:nowrap;
  font-family:var(--f-body);font-weight:500;font-size:.86rem;color:var(--tx-l);
  background:var(--tint);border-radius:var(--r-sm);padding:11px 14px;transition:background .2s,color .2s}
.cat-btn:hover{background:var(--tint-2);color:var(--brand-ink)}
.cat-btn.on{background:var(--brand);color:#fff}
.p-block.alt .cat-btn{background:var(--white)}
.p-block.alt .cat-btn.on{background:var(--brand);color:#fff}
.prod-group[hidden],.gal-group[hidden]{display:none}
.prod-group-h{font-family:var(--f-dsp);font-weight:600;letter-spacing:-.015em;
  font-size:clamp(1.1rem,1.5vw,1.32rem);color:var(--tx-l);margin:clamp(26px,3vw,40px) 0 16px}
.prod-group:first-of-type .prod-group-h,.gal-group:first-of-type .prod-group-h{margin-top:0}

/* ---------- product catalogue ---------- */
.prod-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:clamp(14px,1.6vw,20px)}
.prod{display:flex;flex-direction:column;background:var(--tint);border-radius:var(--r);overflow:hidden;
  transition:transform .3s cubic-bezier(.2,.7,.3,1),box-shadow .3s ease}
.p-block.alt .prod{background:var(--white)}
.prod:hover{transform:translateY(-3px);box-shadow:var(--shadow)}
.prod-media{position:relative;aspect-ratio:4/3;background:#0f1114;overflow:hidden}
.prod-media::after{display:none}
.prod.no-img .prod-media::after{display:none}
.prod-media img{width:100%;height:100%;object-fit:contain;padding:14px}
.prod-body{padding:16px 18px 18px;display:flex;flex-direction:column;gap:10px;flex:1}
.prod-body h4{font-family:var(--f-dsp);font-weight:600;font-size:1rem;letter-spacing:-.01em;color:var(--tx-l);line-height:1.3}
.prod-dl{margin-top:auto;display:inline-flex;align-items:center;gap:8px;font-size:.82rem;font-weight:600;color:var(--brand-ink)}
.prod-dl svg{width:15px;height:15px;flex:none}
.prod-dl:hover{color:var(--brand-deep)}

/* ---------- gallery ---------- */
.gal-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:clamp(14px,1.6vw,22px)}
.gal-item{display:block;text-align:left;width:100%;background:none;border:0;padding:0;font:inherit;color:inherit}
.gal-shot{position:relative;display:block;aspect-ratio:4/3;border-radius:var(--r);overflow:hidden;background:#0f1114}
.gal-shot::after{display:none}
.gal-item.is-ph .gal-shot::after{display:none}
.gal-shot img{width:100%;height:100%;object-fit:cover;transition:transform .5s ease}
button.gal-item{cursor:zoom-in}
button.gal-item:hover .gal-shot img{transform:scale(1.05)}
.gal-item .eframe-cap{margin-top:10px}

/* lightbox — native <dialog>, no library */
.lightbox{border:0;padding:0;background:transparent;max-width:min(94vw,1200px);max-height:92vh;overflow:visible}
.lightbox::backdrop{background:rgba(8,9,10,.88)}
.lightbox img{display:block;max-width:100%;max-height:78vh;width:auto;height:auto;border-radius:var(--r)}
.lightbox .lb-cap{margin-top:12px;color:#C9CDD2!important}
.lb-close{position:absolute;top:-14px;right:-10px;width:40px;height:40px;border-radius:50%;
  background:var(--brand);color:#fff;font-size:24px;line-height:1;display:grid;place-items:center;z-index:2}
.lb-close:hover{background:var(--brand-deep)}

/* ---------- videos ---------- */
/* auto-FILL was the void: it lays down as many 280px tracks as fit (4 at 1440)
   whether or not there are items to occupy them, so a one-clip category showed
   a 394px tile with ~690px of empty tracks beside it. auto-FIT collapses the
   empty tracks, so the tiles always fill the row. 340px min caps the row at
   3 columns, which is the size a real 16:9 thumbnail wants. */
.vid-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,340px),1fr));gap:clamp(16px,2vw,26px)}
/* A one-clip category still has 1152px to fill. Capping the tile only trades a
   horizontal void for a vertical one — at 860px wide it stood 484px tall against
   the three-up row's 206px and dominated a page it should sit level with.
   So the space is filled with content that already exists: for single-clip
   categories the heading and its description move BESIDE the tile instead of
   above it. Same Part A move as the text-led heroes, no copy invented, and the
   tile lands near the two-up size so every category reads at one scale. */
.p-block:has(.vid-grid > .vid:only-child) > .wrap{
  display:grid;grid-template-columns:.85fr 1.15fr;gap:clamp(28px,4vw,56px);align-items:center}
.p-block:has(.vid-grid > .vid:only-child) .p-head{margin-bottom:0;max-width:40ch}
@media (max-width:860px){
  .p-block:has(.vid-grid > .vid:only-child) > .wrap{grid-template-columns:1fr;gap:26px}
  .p-block:has(.vid-grid > .vid:only-child) .p-head{margin-bottom:0}
}
.vid{display:flex;flex-direction:column;gap:12px}
.vid-shot{position:relative;aspect-ratio:16/9;border-radius:var(--r);overflow:hidden;background:#0f1114}
.vid-shot::after{display:none}
.vid-shot video,.vid-shot img{width:100%;height:100%;object-fit:cover}
.vid h3{font-family:var(--f-dsp);font-weight:600;font-size:1.05rem;letter-spacing:-.01em;color:var(--tx-l)}
.vid p{font-size:.9rem;line-height:1.55;color:var(--tx-l-dim)}

@media (max-width:560px){
  .prod-grid{grid-template-columns:repeat(2,minmax(0,1fr))}
  .gal-grid{grid-template-columns:1fr}
}

/* ==========================================================================
   A · THE CALIPER READING — dimension annotation on the bearings hero (2026-07-30)
   Restores the device retired when the homepage hero was rebuilt. It resolves in
   three beats: extension lines, then the dimension line draws across, then the
   readout appears. stroke-dashoffset is paint-only — no layout, no CLS.
   ========================================================================== */
/* the image carried width/height attributes with no CSS height, which is what
   stretched the homepage inspection photo. Fixed here before it can happen. */
.p-hero-shot img{max-width:100%;height:auto;display:block}

.herodim{position:absolute;inset:0;width:100%;height:100%;z-index:2;pointer-events:none;overflow:visible}
.herodim path{fill:none;stroke:var(--brand-graphic);vector-effect:non-scaling-stroke}
.herodim .dim-ext{stroke-width:1.2;opacity:.75;stroke-dasharray:5 4}
.herodim .dim-line{stroke-width:1.8}
.herodim .dim-arrow{fill:var(--brand-graphic);stroke:none}
.herodim .dim-lbl rect{fill:var(--white);stroke:var(--hair-l);stroke-width:1}
.herodim .dim-lbl text{fill:var(--brand-ink);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
  font-size:15px;letter-spacing:.1em}

/* beat 1: extension lines · beat 2: the dimension line draws · beat 3: readout */
.js .herodim .dim-ext{opacity:0}
.js .herodim .dim-line{stroke-dasharray:900;stroke-dashoffset:900}
.js .herodim .dim-arrow,.js .herodim .dim-lbl{opacity:0}
/* ⚠ 2026-08-18 · PART D — the caliper's reveal was keyed ONLY to
   `.p-hero-fig.in`. Once it moved out of the hero, the `.js` rules above
   still HID it and nothing ever revealed it: the annotation was present in
   the DOM at opacity 0, permanently. Same bug class as the framing captions.
   `.specfig.in` is now an equal trigger. */
.p-hero-fig.in .herodim .dim-ext,.specfig.in .herodim .dim-ext{opacity:.75;transition:opacity .4s var(--ease)}
.p-hero-fig.in .herodim .dim-line,.specfig.in .herodim .dim-line{stroke-dashoffset:0;transition:stroke-dashoffset .75s var(--ease) .22s}
.p-hero-fig.in .herodim .dim-arrow,.specfig.in .herodim .dim-arrow{opacity:1;transition:opacity .3s ease .85s}
.p-hero-fig.in .herodim .dim-lbl,.specfig.in .herodim .dim-lbl{opacity:1;transition:opacity .35s ease .95s}

@media (max-width:720px){ .herodim .dim-lbl text{font-size:22px} }
@media (prefers-reduced-motion:reduce){
  .js .herodim .dim-ext{opacity:.75}
  .js .herodim .dim-line{stroke-dasharray:none;stroke-dashoffset:0}
  .js .herodim .dim-arrow,.js .herodim .dim-lbl{opacity:1}
  .p-hero-fig.in .herodim *,.specfig.in .herodim *{transition:none}
}

/* Contact's form/details ratio. Declared only ABOVE the stack breakpoint, so the
   860px rule that collapses .p-split to one column is never fighting it. It was
   previously an inline style, which no media query can override. */
@media (min-width:861px){
  .p-split.p-split-form{grid-template-columns:1.15fr .85fr}
}

/* ==========================================================================
   HERO SUPPORTING ELEMENTS (2026-08-01)
   Fill the right of the fold on text-led pages without widening the 60ch
   measure. NO inline column styles anywhere here — that is what broke
   contact-us on mobile, and a media query cannot override an inline style.
   ========================================================================== */
.p-hero-aside{min-width:0;align-self:center}

/* facts — a short block of figures already published on the same page */
.p-hero-facts{display:flex;flex-direction:column}
.p-hero-facts div{padding:16px 0;border-top:1px solid var(--hair-l)}
.p-hero-facts div:first-child{border-top:0;padding-top:0}
.p-hero-facts b{display:block;font-family:var(--f-dsp);font-weight:600;letter-spacing:-.02em;
  font-size:clamp(1.05rem,1.5vw,1.32rem);color:var(--brand-ink);
  font-variant-numeric:tabular-nums;margin-bottom:5px}
.p-hero-facts span{display:block;font-size:.88rem;line-height:1.45;color:var(--tx-l-dim)}

/* figure slot — the void becomes a frame the 5 Aug photograph drops into */
.p-hero-slot .ph-media{width:100%;aspect-ratio:4/3;border-radius:var(--r);min-height:0}

/* line-art at real strength — a deliberate element, not the 5% background wash */
.p-hero-art{position:relative;aspect-ratio:4/3;overflow:hidden;border-radius:var(--r);
  background:var(--tint);color:var(--ink);opacity:1}
.p-hero-art .la-field{position:absolute;width:150%;height:150%;left:-22%;top:-25%;opacity:.20}

@media (max-width:860px){
  /* stacks below the copy; the class-based ratio in the shared rule collapses too */
  .p-hero-aside{align-self:start}
  .p-hero-art{aspect-ratio:16/9}
  .p-hero-facts{margin-top:4px}
}

/* ==========================================================================
   §M · MOBILE PASS (2026-08-12) — inner-page components only
   Appended, never spliced. pages.css is enqueued AFTER app.css on inner pages,
   so anything here that also exists in app.css wins on source order — which is
   exactly why this rule has to live here and not there.
   ========================================================================== */
@media (max-width:720px){
  /* `.p-cell .n` is not decoration, it is CONTENT: the process names on the
     division pages ("Centrifugal Casting", "Bond Preparation & Testing"). At
     .68rem it was 10.9px — the smallest real copy anywhere on the site. */
  .p-cell .n{font-size:12.2px;letter-spacing:.06em}
  /* The 36 datasheet buttons on /specialised-products/ measured 42px. They are
     not `.cta`, so the sitewide button floor did not reach them — 108 misses
     across the three phone widths, all from this one selector. */
  .prod-dl{min-height:44px}
  /* the blog card's date + read-time line — 10.9px */
  .bcard-meta{font-size:12px;letter-spacing:.05em}
}

/* CASCADE FIX (2026-08-12, from the proactive collision audit).
   `.p-consent input` is declared HERE with no media query and in app.css inside
   `@media (max-width:720px)`. Specificity is identical, so source order decides
   — and pages.css is enqueued AFTER app.css, which meant the mobile 22px target
   silently lost on every inner page and the RFQ / contact checkbox stayed 18px.
   Verified as a live defect by measuring /rfq/ at 414 (18x18) BEFORE fixing.
   The sweep missed it because it excludes checkboxes by design: the box is meant
   to stay small and its LABEL carries the 44px target. The audit caught it. */
@media (max-width:720px){
  .p-consent input{width:22px;height:22px}
}

/* ==========================================================================
   §V · INNER-PAGE VISUAL PASS (2026-08-12) — flagship: /rotating-equipment/
   Appended, never spliced. Purely visual: layout, scale, rhythm, image
   treatment. NOT ONE WORD OF COPY IS TOUCHED by anything in this block, and no
   section is added or reordered — every rule below restyles markup that is
   already on the page.
   Written as a LANGUAGE, not a page: every selector here is a shared inner-page
   component (.p-hero, .p-split, .p-table, .p-gallery, .p-cell), so approving the
   flagship approves the treatment for all 15 inner pages at once.
   ========================================================================== */

/* --------------------------------------------------------------------------
   §V1 · THE HERO — give the photograph presence, and stop the mobile sprawl
   Measured before, at 1340: the hero ran 745px tall while its content occupied
   ~450px, and the product photograph rendered at 462x355 inside a ~490px
   column — a thumbnail on the page whose entire subject is that component.
   At 390 the hero was 1186px, i.e. 1.5 screens, with both CTAs below the fold —
   the same defect the homepage hero had before the mobile pass, in the same
   shared component.
   -------------------------------------------------------------------------- */
/* ⚠ MUST be min-width scoped. Written first as an unscoped rule, it sat LATER in
   the file than the existing `@media (max-width:860px){.p-hero.has-img
   .p-hero-grid{grid-template-columns:1fr}}` and beat it at equal specificity — so
   the hero stayed two-column at 390, the copy column collapsed to ~190px and the
   headline fell into SEVEN lines with the caption card over the lead. Nothing
   overflowed, so only looking at 390 caught it. Same cascade class as the five
   already on file, this time self-inflicted by appending. */
@media (min-width:861px){
  /* ⚠ 2026-08-17 — this padding is INSIDE a min-width query and sits later in
     the file than the base .p-hero-grid rule, so it beats it on desktop. When
     the base was retuned to the ATF band the first edit changed nothing here;
     caught by re-measuring, which is the rule. Same band as the base now. */
  .p-hero.has-img .p-hero-grid{grid-template-columns:1.12fr .88fr;align-items:center;
    padding-block:clamp(34px,3.6vw,52px)}
}
/* ⚠ 1fr/1fr was tried first and REGRESSED the hero: it narrowed the copy column
   from 634 to 552px, and the two CTAs need ~604px side by side, so the ghost
   dropped to its own line. Caught by looking at the render, not by any metric —
   nothing overflowed. 1.12/.88 keeps the pair inline with ~24px to spare while
   the photograph still gains on its previous 462px. Most of the presence comes
   from the frame and shadow below, not from raw pixels. */
/* the figure carries the section now, so it gets a real frame and a shadow that
   lifts it off the tint rather than sitting flush on it */
.p-hero.has-img .p-hero-shot{border-radius:var(--r-lg);overflow:hidden;
  box-shadow:0 18px 48px -24px rgba(8,9,10,.42)}
.p-hero h1{margin-top:14px}

@media (max-width:720px){
  /* Same medicine as the homepage hero, applied to the shared inner-page one:
     the LINE was wrong, not the length. 16px/1.5 gives ~44 characters a line. */
  .p-hero .p-hero-grid{padding-block:clamp(24px,5vw,38px) clamp(22px,4vw,32px);gap:24px}
  .p-hero-copy .lead{font-size:1rem;line-height:1.52;margin-top:14px}
  .p-hero h1{margin-top:10px}
  /* ⚠ Written first as `.p-hero-cta`, which DOES NOT EXIST — the row is
     `.p-cta` (emitted by sacom_page_hero() in blocks.php, defined in app.css).
     The rule shipped, changed nothing, and was only caught because measuring
     the desktop CTA row printed the parent's real class name. Sixth selector
     this project has had that was written, deployed and inert. */
  .p-hero .p-cta{margin-top:20px;gap:12px 18px}
  .p-hero .p-cta .cta:not(.ghost){flex:1 0 100%;justify-content:center;min-height:48px}
}

/* --------------------------------------------------------------------------
   §V2 · .p-split — THE VOID, CLOSED BY REBUILDING THE LAYOUT
   The audit logged this as "systemic across 19 uses" and then, correctly,
   declined a blanket fix because most instances measure <=46px. On THIS page
   both instances are the bad case: a two-line heading in the left column against
   two full paragraphs on the right, leaving ~500px of dead white beside the
   reading path at 1340.
   The fix is not to fill the column — there is no copy to fill it with, and
   inventing some would breach content fidelity. It is to stop having a column:
   the heading goes FULL WIDTH as an editorial lead-in and the prose sets in two
   measured columns beneath it. That reads as designed, uses the whole canvas,
   and adds nothing.
   ⚠ Scoped with :has() to instances with 2+ paragraphs. A one-paragraph
   .p-split still has enough prose to balance its heading, and forcing it into
   two columns would split a short paragraph down the middle — worse than the
   void it replaced.
   Counted across the templates before shipping, not assumed: 19 .p-split
   instances, of which 9 carry 2+ paragraphs and take this layout (about-us x3,
   automation x2, cutting-tools x2, precision-machining, rotating-equipment) and
   10 carry 0-1 and are deliberately untouched. So this page's Introduction is
   restyled and its one-paragraph "Reverse engineering" section is not.
   -------------------------------------------------------------------------- */
@media (min-width:900px){
  .p-split:has(.prose p + p){grid-template-columns:1fr;gap:clamp(22px,2.4vw,34px)}
  .p-split:has(.prose p + p) h2{max-width:22ch}
  /* ⚠ `.prose` carries its own max-width (computed 646px), so `columns:2` set
     two 310px columns inside a 1152px container and simply MOVED the void from
     the left column to the right-hand 500px — with a full-width hairline above
     it pointing at the emptiness. Measured, not guessed.
     Column COUNT is also wrong here: with break-inside:avoid, two paragraphs can
     only ever make two columns, so filling the width would force a ~85-character
     measure. Column WIDTH instead lets the text flow and the browser choose —
     3 columns at 1340, 2 at ~900 — at a 55-character measure throughout. */
  .p-split:has(.prose p + p) .prose{display:block;max-width:none;
    column-width:clamp(280px,25vw,360px);column-gap:clamp(30px,3.4vw,54px)}
  .p-split:has(.prose p + p) .prose p{max-width:none;margin:0 0 16px}
  .p-split:has(.prose p + p) .prose p:last-child{margin-bottom:0}
  /* a hairline under the lead-in ties the heading to the columns it introduces */
  .p-split:has(.prose p + p) > :first-child{padding-bottom:clamp(18px,2vw,26px);
    border-bottom:1px solid var(--hair-l)}
}

/* --------------------------------------------------------------------------
   §V3 · .p-table — the page's substance, currently its plainest element
   This table IS the answer to "what do you actually make". It was 12 rows of
   unstyled text on hairlines. Same information, given the technical-document
   treatment the rest of the site already uses: a brand rule at the head, mono
   tracked column labels, tint on the row you are reading, and room to breathe.
   -------------------------------------------------------------------------- */
.p-table{font-size:1rem;border-top:1px solid var(--hair-l)}
.p-table thead th{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
  font-size:.7rem;letter-spacing:.12em;color:var(--brand-ink);padding-top:18px;padding-bottom:14px}
.p-table th,.p-table td{padding:18px 20px}
.p-table tbody tr{transition:background .2s var(--ease)}
.p-table tbody tr:hover{background:var(--tint)}
.p-table tbody th{white-space:normal;font-size:1.02rem;letter-spacing:-.01em}
.p-table tbody th::before{content:"";display:inline-block;width:6px;height:6px;
  background:var(--brand-graphic);margin-right:12px;vertical-align:middle;flex:none}
.p-table caption{padding-bottom:14px}
@media (max-width:720px){
  .p-table th,.p-table td{padding:14px 12px}
  .p-table{font-size:.95rem}
}

/* --------------------------------------------------------------------------
   §V4 · .p-gallery — the real photography, at a size worth looking at
   `minmax(230px,1fr)` auto-filled to FOUR columns at 1340, rendering the eight
   real Shaheen photographs at 272x204. On the page whose whole argument is
   "we make this part", the evidence was postage stamps. Three up at ~400px,
   and the frames now respond to the pointer because they open a lightbox.
   -------------------------------------------------------------------------- */
.p-gallery{grid-template-columns:repeat(auto-fill,minmax(300px,1fr));
  gap:clamp(18px,2vw,30px)}
@media (min-width:1000px){ .p-gallery{grid-template-columns:repeat(3,1fr)} }
.p-gallery .gal-item{cursor:zoom-in;display:block;width:100%;
  background:none;border:0;padding:0;font:inherit;color:inherit}
.p-gallery .eframe{transition:transform .45s var(--ease),box-shadow .45s var(--ease)}
.p-gallery .gal-item:hover .eframe{transform:translateY(-4px);
  box-shadow:0 22px 46px -26px rgba(8,9,10,.5)}
.p-gallery .gal-item:focus-visible{outline:2px solid var(--brand-graphic);outline-offset:4px;border-radius:var(--r-lg)}
.p-gallery .eframe img{transition:transform .6s var(--ease)}
.p-gallery .gal-item:hover .eframe img{transform:scale(1.03)}
@media (prefers-reduced-motion:reduce){
  .p-gallery .eframe,.p-gallery .eframe img{transition:none!important}
  .p-gallery .gal-item:hover .eframe{transform:none}
  .p-gallery .gal-item:hover .eframe img{transform:none}
}

/* --------------------------------------------------------------------------
   §V5 · .p-cell — the process cards, given a technical read
   Four equal cards of plain text. The label is already `.n` (mono, tracked,
   brand green); it now sits over a rule so each card reads as an entry in a
   process sheet rather than a paragraph in a box.
   -------------------------------------------------------------------------- */
.p-cell .n{display:block;padding-bottom:12px;border-bottom:1px solid var(--hair-l)}
.p-cell{border:1px solid transparent;transition:border-color .3s var(--ease),
  transform .3s var(--ease),box-shadow .3s var(--ease)}
.p-cell:hover{border-color:rgba(4,105,56,.18)}

/* ==========================================================================
   §P · THREE INTERACTIVE DIRECTIONS — PREVIEWS (2026-08-12)
   Only ever rendered behind ?preview=a|b|c. Appended, never spliced.
   ⚠ These are built on their OWN rules rather than "reusing" the homepage's
   .caps-seq / .exp-* components, because that CSS lives in home.css which is
   not enqueued on inner pages — the page-scoped-stylesheet trap this project
   has already hit four times. Reuse here would have looked correct in the
   editor and rendered unstyled on the page.
   ========================================================================== */
.pv{position:relative}

/* ---------- A · component explorer ---------- */
.pv-a-grid{display:grid;grid-template-columns:1fr;gap:clamp(24px,3vw,40px)}
@media (min-width:900px){ .pv-a-grid{grid-template-columns:.82fr 1.18fr;align-items:start} }
.pv-a-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column}
.pv-a-item{display:flex;align-items:center;gap:12px;width:100%;text-align:left;
  background:none;border:0;border-bottom:1px solid var(--hair-l);padding:13px 4px;
  font:inherit;font-size:.98rem;color:var(--tx-l-dim);cursor:pointer;min-height:46px;
  transition:color .25s var(--ease),padding-left .25s var(--ease)}
.pv-a-item .pv-a-tick{width:7px;height:7px;flex:none;background:var(--brand-graphic);
  opacity:.22;transition:opacity .25s var(--ease),transform .25s var(--ease)}
.pv-a-item:hover{color:var(--tx-l);padding-left:10px}
.pv-a-item.on{color:var(--tx-l);font-weight:600;padding-left:10px}
.pv-a-item.on .pv-a-tick{opacity:1;transform:scale(1.5)}
/* ⚠ WAS `opacity:.72`, which measured 3.42 against the 4.5 it needs once the
   alpha was COMPOSITED — the raw colour reads 6.52 and hides the failure. On
   this palette --tx-l-dim needs alpha 0.85 just to stay compliant, so dimming
   body text by opacity is not a tool that exists here. The distinction is
   carried by the tick instead, which is a graphic and only needs 3:1. */
.pv-a-item.no-shot .pv-a-tick{background:none;box-shadow:inset 0 0 0 1.5px var(--brand-graphic);opacity:.5}
.pv-a-item:focus-visible{outline:2px solid var(--brand-graphic);outline-offset:2px}
.pv-a-stage{position:relative;min-width:0}
.pv-a-panel{margin:0}
.pv-a-panel[hidden]{display:none}
.pv-a-shot{position:relative;border-radius:var(--r-lg);overflow:hidden;background:#0f1114;aspect-ratio:4/3}
.pv-a-shot img{width:100%;height:100%;object-fit:contain;padding:clamp(10px,1.4vw,20px)}
.pv-a-shot.is-none{display:grid}
.pv-a-leader{position:absolute;inset:0;width:100%;height:100%;pointer-events:none;overflow:visible}
.pv-a-leader path{fill:none;stroke:var(--brand-lift);stroke-width:1.4;vector-effect:non-scaling-stroke}
.pv-a-leader .pv-l-dot{fill:var(--brand-lift)}
.pv-a-leader .pv-l-tick{opacity:.6}
/* the draw-on: paint-only, one property */
.js .pv-a-leader .pv-l-arc{stroke-dasharray:1;stroke-dashoffset:1}
.js .pv-a-leader .pv-l-dot,.js .pv-a-leader .pv-l-tick{opacity:0}
.pv-a-panel.on .pv-l-arc{stroke-dashoffset:0;transition:stroke-dashoffset .7s var(--ease) .15s}
.pv-a-panel.on .pv-l-dot{opacity:1;transition:opacity .4s ease .5s}
.pv-a-panel.on .pv-l-tick{opacity:.6;transition:opacity .4s ease .55s}

/* ---------- C · blueprint over photograph + spec readout ---------- */
.pv-c-stage{display:grid;grid-template-columns:1fr;gap:clamp(26px,3.4vw,46px);align-items:center}
@media (min-width:1000px){ .pv-c-stage{grid-template-columns:1.1fr .9fr} }
.pv-c-shot{position:relative;border-radius:var(--r-lg);overflow:hidden;background:#0b0d0e;aspect-ratio:4/3}
.pv-c-shot img{width:100%;height:100%;object-fit:contain;padding:clamp(12px,1.6vw,24px)}
.pv-c-scrim{position:absolute;inset:0;pointer-events:none;
  background:radial-gradient(72% 72% at 50% 50%,rgba(8,9,10,0) 0%,rgba(8,9,10,.42) 78%,rgba(8,9,10,.66) 100%)}
.pv-c-bp{position:absolute;inset:0;width:100%;height:100%;pointer-events:none}
.pv-c-bp g{fill:none;stroke:var(--brand-lift);stroke-width:1;vector-effect:non-scaling-stroke;opacity:.5}
.pv-c-bp .pv-c-dash{stroke-dasharray:10 6;opacity:.7}
.pv-c-bp .pv-c-dot{fill:var(--brand-lift);stroke:none;opacity:.9}
/* draw-on: pathLength normalised on the GROUP, one animated element */
.js .pv-c-bp-g{stroke-dasharray:1;stroke-dashoffset:1}
.pv-c.in .pv-c-bp-g{stroke-dashoffset:0;transition:stroke-dashoffset 1.6s var(--ease) .2s}
.pv-c-bp .pv-c-bp-static{fill:none;stroke:var(--brand-lift);stroke-width:1;
  vector-effect:non-scaling-stroke;opacity:.5}
.js .pv-c-bp-static{opacity:0}
.pv-c.in .pv-c-bp-static{opacity:.5;transition:opacity .6s ease 1.3s}
.pv-c-spec{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:clamp(18px,2.2vw,30px)}
.pv-c-spec li{padding-top:16px;border-top:1px solid var(--hair-d)}
.pv-c-spec b{display:block;font-family:var(--f-dsp);font-weight:600;line-height:1;
  font-size:clamp(2.1rem,4.4vw,3.2rem);letter-spacing:-.028em;color:var(--tx-d);font-variant-numeric:tabular-nums}
.pv-c-spec .accent{color:var(--brand-lift)}
.pv-c-spec .u{display:block;margin-top:10px;font-size:.95rem;color:var(--tx-d-dim)}
.pv-c-spec i{display:block;margin-top:4px;font-style:normal;
  font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.7rem;
  letter-spacing:.13em;text-transform:uppercase;color:var(--brand-lift)}
@media (prefers-reduced-motion:reduce){
  .js .pv-c-bp-g,.pv-c-bp-g{stroke-dasharray:none!important;stroke-dashoffset:0!important;transition:none!important}
  .js .pv-c-bp-static,.pv-c-bp-static{opacity:.5!important;transition:none!important}
}

/* ==========================================================================
   §B · THE ADOPTED BEARINGS BLEND (2026-08-12)
   C's blueprint in the hero, C's readout on the light canvas, A's explorer as
   the components section. Appended, never spliced.
   ========================================================================== */

/* ---------- C · the blueprint over the real bearing ---------- */
.herobp{position:absolute;inset:0;width:100%;height:100%;z-index:1;pointer-events:none}
.herobp g{fill:none;stroke:var(--brand-lift);stroke-width:1;vector-effect:non-scaling-stroke;opacity:.42}
.herobp .herobp-dash{stroke-dasharray:10 6;opacity:.55}
.herobp .herobp-dot{fill:var(--brand-lift);stroke:none;opacity:.85}
/* one animated element: pathLength=1 on every path, offset inherited from the group */
.js .herobp-g{stroke-dasharray:1;stroke-dashoffset:1}
.js .herobp-static{opacity:0}
.p-hero-fig.in .herobp,.specfig.in .herobp-g{stroke-dashoffset:0;transition:stroke-dashoffset 1.5s var(--ease) .35s}
.p-hero-fig.in .herobp,.specfig.in .herobp-static{opacity:1;transition:opacity .6s ease 1.35s}
/* the caliper annotation stays ON TOP of the geometry — it is the approved device */
.herodim{z-index:2}

/* ---------- C · the spec readout, on the LIGHT canvas ----------
   ⚠ THE ONE-INVERTED-SECTION RULE, RESOLVED. The preview put this on charcoal,
   which would have given the page TWO dark sections against the hard limit — the
   warranty band already holds that moment, and it holds it better, because the
   8,000-hour warranty IS the page's emphasis. So the readout stays light and
   earns its weight from scale and a brand rule instead. Nothing competes, and
   the page still has exactly one inverted section. */
.specrow{padding-block:var(--sp)}   /* was a bespoke 30-48 clamp that bypassed the scale */
.specrow .pv-c-spec{display:grid;grid-template-columns:1fr;gap:clamp(20px,2.4vw,34px)}
@media (min-width:820px){ .specrow .pv-c-spec{grid-template-columns:repeat(3,1fr)} }
.pv-c-spec{list-style:none;margin:0;padding:0}
.pv-c-spec li{padding-top:16px;border-top:1px solid var(--hair-d)}
.pv-c-spec b{display:block;font-family:var(--f-dsp);font-weight:600;line-height:1;
  font-size:clamp(2rem,3.6vw,2.9rem);letter-spacing:-.028em;color:var(--tx-l);
  font-variant-numeric:tabular-nums}
.pv-c-spec .accent{color:var(--brand-ink)}
.pv-c-spec .u{display:block;margin-top:10px;font-size:.95rem;color:var(--tx-l-dim)}
.pv-c-spec i{display:block;margin-top:4px;font-style:normal;
  font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.7rem;
  letter-spacing:.13em;text-transform:uppercase;color:var(--brand-ink)}

/* ---------- A · the component schematics ----------
   One drawing language across all eight, matching the hero geometry's weight so
   a schematic panel and a photograph panel read as the same sheet. */
.pv-a-shot.is-draw{background:#0f1114;display:grid;place-items:center}
/* Only `color` needs to reach the schematic: `stroke="currentColor"` is written
   on every element in the sprite, and `color` is inheritable so it DOES cross
   the <use> shadow boundary. Everything else is a presentation attribute — see
   the note in component-lineart.svg.php for why. */
.cl-fig{width:100%;height:100%;display:block;padding:clamp(12px,1.6vw,26px);
  color:var(--brand-lift)}
/* The schematics FADE rather than draw on. A stroke-dashoffset draw-on cannot be
   targeted per-element through <use>, and applying one uniformly would also
   overwrite the dashed centrelines' own pattern. A cross-fade with a slight
   settle is the same entrance the panels already use, and it costs one property. */
.js .pv-a-panel .cl-fig{opacity:0;transform:scale(1.02)}
.pv-a-panel.on .cl-fig{opacity:1;transform:none;
  transition:opacity .5s var(--ease),transform .7s var(--ease)}

@media (prefers-reduced-motion:reduce){
  .js .herobp-g,.herobp-g{stroke-dasharray:none!important;stroke-dashoffset:0!important;transition:none!important}
  .js .herobp-static,.herobp-static{opacity:1!important;transition:none!important}
  .js .pv-a-panel .cl-fig,.pv-a-panel .cl-fig{opacity:1!important;transform:none!important;transition:none!important}
}

/* ==========================================================================
   §S2 · SECTOR THUMBNAILS — measured lift (2026-08-16)
   The eight real sector frames measure mean luminance 44-80/255 with stdev
   34-57. That reads correctly at homepage tile scale (a full-width image with
   an overlaid title) but at the 196px inner-page thumbnail the subject loses
   separation from the plate. A GPU filter lifts it — no re-encode, so the
   homepage tiles, which are graded for their own scrim, are untouched.
   ========================================================================== */
.sector-media img{filter:brightness(1.14) contrast(1.05) saturate(1.04)}

/* ==========================================================================
   §W · THE DEAD-SPACE FAMILY, CLOSED PROPERLY (2026-08-17)
   Bhavana reviewed and reported large empty voids, and that a previous fix
   "didn't show any difference". Both halves of that are true, and the reason
   is worth writing down rather than paraphrasing.

   §V2 (2026-08-12) DID deploy and DID work — it is live and correct. It is
   scoped `.p-split:has(.prose p + p)`, i.e. two or more PARAGRAPHS, and its own
   comment records the decision: "10 carry 0-1 and are deliberately untouched",
   on the reasoning that a one-paragraph column still balances its heading.

   Re-measured on the served page at 1352, that reasoning holds everywhere
   except one instance, and the exception is the one she screenshotted:
     /precision-machining/  "Why Local Machining Matters"  124 vs 128  -> fine
     /industries/           "Why Local Capability Matters"  balanced   -> fine
     /automation.../        "Why It Matters"                balanced   -> fine
     /vision-2030/          "Why It Matters"    heading 82 vs prose 665
                                                -> 583px OF EMPTY BLACK
   So the guard was not wrong about paragraph count; it was measuring the wrong
   thing. What makes a prose column tall enough to strand its heading is not a
   second PARAGRAPH, it is a FIGURE — and `p + p` cannot see a figure. Every
   other p-split carrying a figure happens to carry two paragraphs as well and
   so was already caught; vision-2030 carries one paragraph and one figure, and
   fell through the only gap in the set.

   Fixed by matching the actual cause. Nothing is invented and no copy moves:
   the heading becomes a full-width lead-in (the §V2 language, so the two read
   as one system) and the paragraph and the figure sit side by side beneath it.
   The section stops having a short column at all, so there is no void to fill.
   ========================================================================== */
@media (min-width:900px){
  /* the cause, stated as a selector: a prose column carrying a figure */
  .p-split:has(.prose > figure){grid-template-columns:1fr;gap:clamp(22px,2.4vw,34px)}
  .p-split:has(.prose > figure) h2{max-width:22ch}
  /* same hairline lead-in §V2 uses, so the two treatments are one language */
  .p-split:has(.prose > figure) > :first-child{padding-bottom:clamp(18px,2vw,26px);
    border-bottom:1px solid var(--hair-l)}
  .p-block.dark .p-split:has(.prose > figure) > :first-child{border-bottom-color:var(--hair-d)}
  /* the prose column becomes the row: text, then the figure beside it.
     ⚠ An earlier version of this comment claimed the change also ENLARGES the
     photograph. Re-measured on the served page, that is wrong and is corrected
     here: the frame goes 626 -> 593px wide, i.e. very slightly smaller. What
     the fix buys is not size, it is composition — 583px of dead black removed
     and the section closing on one flush edge. Do not repeat that claim.
     ⚠ `align-items:start` was tried first and measured: paragraph 159px beside
     a 472px figure left 313px of black under the text — a smaller version of
     the same defect, in a new place. Centring splits that space above and below
     the paragraph, so it reads as a set column rather than a short one. */
  .p-split:has(.prose > figure) .prose{display:grid;max-width:none;
    grid-template-columns:1fr 1.15fr;gap:clamp(26px,3.2vw,52px);align-items:center}
  .p-split:has(.prose > figure) .prose > p{max-width:52ch;margin:0;
    font-size:clamp(1.02rem,1.18vw,1.14rem);line-height:1.62}
  .p-split:has(.prose > figure) .prose > figure{margin:0}
  /* ⚠ the figure carries `style="margin-top:24px"` inline from the template.
     Inline style beats a stylesheet, so the frame would sit 24px below the
     paragraph's first line and re-introduce a small step. Copy is verbatim and
     templates are not being edited for layout, so it is neutralised here. */
  .p-split:has(.prose > figure) .prose > figure[style]{margin-top:0!important}
}
/* Below 900 the grid is already one column (line 290) and .prose stacks, which
   is the correct phone order: heading, then text, then the photograph. */
@media (max-width:899px){
  .p-split:has(.prose > figure) .prose{display:flex;flex-direction:column;gap:16px}
}

/* --------------------------------------------------------------------------
   §W2 · THE COMPONENT PANEL NAMES ITSELF (2026-08-17)
   Bhavana's note on the component explorer was "what's this". Looking at the
   built panel, that is a fair question and the page was not answering it: the
   only identification was a small grey caption BELOW a 656x492 black frame,
   and that caption is `.eframe-cap`, which starts at opacity 0 and waits for an
   IntersectionObserver to add `.rv`. Eleven of the twelve panels start life
   inside `hidden`, where an observer cannot fire — measured on the served page:
   19 captions with rv=false and computed opacity 0.
   ⚠ Careful here — the FIRST reading of that was wrong and nearly shipped as a
   bug report. The visible panel's caption was mid-transition in a backgrounded
   tab (playState "running", currentTime 15.8ms) and finishing the animation
   resolved it to opacity 1. The frozen-timeline trap, exactly as CLAUDE.md
   describes. So the caption mechanism is NOT broken.
   It is, however, the wrong thing to hang identification on. The plate below is
   plain markup with no gate, no observer and no transition, so the panel states
   what it is the instant it is shown, in every path including no-JS.
   -------------------------------------------------------------------------- */
.pv-a-plate{position:absolute;left:0;top:14px;z-index:4;max-width:82%;
  font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
  font-size:10.5px;letter-spacing:.13em;text-transform:uppercase;line-height:1.35;
  color:rgba(255,255,255,.94);background:rgba(8,9,10,.74);
  padding:6px 12px 6px 14px;
  border-radius:0 3px 3px 0}
/* The designation sits opposite the name so a drawing announces itself as a
   drawing without the two labels ever colliding on a narrow panel. */
.pv-a-desig{position:absolute;right:12px;bottom:12px;z-index:4;
  font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
  font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;
  padding:4px 9px;border-radius:3px;
  color:rgba(255,255,255,.92);background:rgba(8,9,10,.72)}
/* The provenance tag already renders bottom-RIGHT on a photograph panel; a
   schematic has no tag, so the designation takes that corner and they never
   coexist. Verified against the rendered markup, not assumed. */
@media (max-width:720px){
  .pv-a-plate{font-size:11px;top:10px;padding:5px 10px 5px 11px;max-width:88%}
  .pv-a-desig{font-size:9px;right:9px;bottom:9px}
}

/* --------------------------------------------------------------------------
   §W3 · THE BLOG HERO ASIDE EARNS ITS PLACE (2026-08-17)
   Replaces the line-art field on /blog/ with the newest article. Same slot,
   same 4:3 reservation, so the hero geometry and CLS behaviour are unchanged —
   only the contents go from decorative to useful.
   The card is ONE <a>, so the whole panel is the target and there is no
   nested-interactive problem; the inner elements are spans for that reason.
   -------------------------------------------------------------------------- */
.p-hero-latest{align-self:center;min-width:0}
.hl-card{display:flex;flex-direction:column;gap:0;text-decoration:none;color:inherit;
  border:1px solid var(--hair-l);border-radius:var(--r);overflow:hidden;
  background:var(--white);transition:border-color .3s var(--ease),
  box-shadow .35s var(--ease),transform .35s var(--ease)}
.hl-card:hover{border-color:rgba(4,105,56,.34);transform:translateY(-3px);
  box-shadow:0 20px 44px -26px rgba(8,9,10,.36)}
.hl-card:focus-visible{outline:2px solid var(--brand-graphic);outline-offset:3px}
.hl-media{display:block;position:relative;aspect-ratio:16/9;overflow:hidden;background:#0f1114}
/* cover is correct HERE and only here: a blog thumbnail is an atmospheric
   image with no product to slice. Component and tool frames stay contain. */
.hl-media img{display:block;width:100%;height:100%;object-fit:cover;
  transition:transform .6s var(--ease)}
.hl-card:hover .hl-media img{transform:scale(1.04)}
.hl-media::after{display:none}
.hl-body{display:block;padding:clamp(14px,1.5vw,20px)}
.hl-kicker{display:block;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
  font-size:11px;letter-spacing:.15em;text-transform:uppercase;color:var(--brand-ink)}
.hl-meta{display:block;margin-top:8px;font-size:.8rem;color:var(--tx-l-dim)}
.hl-title{display:block;margin-top:6px;font-family:var(--f-dsp);font-weight:600;
  font-size:clamp(1.02rem,1.25vw,1.18rem);line-height:1.32;letter-spacing:-.012em;
  color:var(--tx-l)}
.hl-go{display:inline-flex;align-items:center;gap:8px;margin-top:12px;
  font-size:.86rem;font-weight:600;color:var(--brand-ink)}
.hl-card:hover .hl-go{gap:12px}
@media (prefers-reduced-motion:reduce){
  .hl-card,.hl-media img,.hl-go{transition:none!important}
  .hl-card:hover{transform:none}
  .hl-card:hover .hl-media img{transform:none}
}
@media (max-width:860px){
  /* the hero has already stacked; the card sits under the copy at full width
     and its media flattens so it does not push the CTAs a screen further down */
  .hl-media{aspect-ratio:21/9}
}

/* --------------------------------------------------------------------------
   §W4 · IMAGE CONTAINMENT — THE PRODUCT IS NEVER SLICED (2026-08-17)
   Abhay's point, and Bhavana's, and it is measurable rather than a matter of
   taste. `.bcard-media` declared aspect-ratio:16/10 and cover-fitted whatever
   thumbnail a post carried. Measured on the served /blog/ at 1352, against the
   eight real featured images:
       inspection.webp    0.91 AR in a 1.60 box -> 43% of the frame cut
       reamer.webp        1.00                  -> 38%   (a cutting tool)
       reverse-eng.webp   1.01                  -> 37%
       tilting-pad.webp   1.30                  -> 19%   (a bearing)
       turbine-thrust.webp1.33                  -> 17%   (a bearing)
       cnc-grinding /facility-floor /poster ~1.78 -> 10-11%
   Five of the eight are photographs of a PART, and a part with 17-38% of it
   outside the frame reads as a mistake, which is exactly the report.
   No single fixed box fixes this: the set spans 0.91 to 1.80: any ratio that
   suits one end guarantees a bad crop at the other. So the box stops cropping.
   `contain` on the plate these cards ALREADY have — the .bcard-media background
   is a dark radial gradient, i.e. it was designed for a contained subject — is
   the same treatment .prod-media and .pv-a-shot use for the same reason.
   The aspect box is retained, so this is a containment change with ZERO CLS.
   -------------------------------------------------------------------------- */
.bcard-media{aspect-ratio:4/3}
.bcard-media img{object-fit:contain;padding:clamp(10px,1.4vw,18px)}
/* Same rule for the hero's latest-article card: its thumbnail comes from the
   very same pool, and a 16/9 box measured 49% off the portrait one. */
.hl-media{aspect-ratio:4/3;background:radial-gradient(70% 70% at 50% 45%,#20242b 0%,#0f1114 78%)}
.hl-media img{object-fit:contain;padding:clamp(10px,1.3vw,16px)}
@media (max-width:860px){ .hl-media{aspect-ratio:16/9} }
/* ⚠ `sacom_arrow()` ships an UNSIZED <svg>. Everywhere else it is used it sits
   inside .cta / .rd / .link-ar, which size it. Inside .hl-go it rendered at its
   intrinsic 54x54 and shoved "Read the article" onto three lines — visible in
   the first render, and the seventh time this project has had a rule or a
   helper that behaved correctly only in the context it was built in. */
.hl-go{white-space:nowrap}
.hl-go svg{width:16px;height:16px;flex:none}

/* §W4b · the per-image containment class (see sacom_fit_class() in blocks.php).
   Applied ONLY to frames whose image would otherwise be cut by more than the
   tolerance, so the ~40 images that already fit their 4:3 frame exactly are
   completely untouched and the grids keep their filled rhythm. */
/* §Y1 · CONTAINED, BUT NOT IN A PADDED BOX (2026-08-17)
   ⚠ REPLACES yesterday's treatment, on Abhay's note. Yesterday's fix was right
   about the crop and wrong about the presentation: it stopped cutting the
   product, but it did it with `contain` + up to 30px of padding on a flat
   plate, which reads as a small photograph floating inside a bordered box.
   Both things have to be true at once — uncropped AND edge to edge.
   The frame now paints a BLURRED, SCALED COPY OF THE SAME PHOTOGRAPH as its
   ground (the `--shot` custom property, emitted per frame in PHP), with the
   sharp contained image on top. The frame fills corner to corner, the subject
   is complete, and there is no visible padding band because the ground is the
   picture's own colour and light rather than a plate.
   The blurred layer is `aria-hidden` by construction (a ::before), costs no
   extra request (same file, already cached) and is composited on the GPU. */
/* ⚠ 2026-08-18 · PART B — `.fit-whole` and `.contain` had DIVERGED into two
   different contain treatments: this one painted a DARK blurred-self ground
   with 6px of inset, while `.eframe.contain` had moved to the light product
   ground. A row could therefore hold one full-bleed tile, one letterboxed on
   dark, and one on light — which is exactly the "off / not properly fixed"
   look. They are now ONE treatment. Most assets no longer reach this path at
   all: the ten non-4:3 sources were padded to true 4:3 against their own
   background, so they cover edge-to-edge and never letterbox. */
.gal-shot.fit-whole,.eframe.fit-whole{background:var(--shot-ground);isolation:isolate}
.gal-shot.fit-whole img,.eframe.fit-whole img{
  position:relative;z-index:1;object-fit:contain;padding:0}
/* a hair of inset ONLY where the subject would otherwise touch the frame edge,
   far below the padded-box threshold — 30px read as a border, 6px reads as air */
.gal-shot.fit-whole img,.eframe.fit-whole img{padding:0}
/* The margin is deliberate. Bhavana cited falcongroup.ae/custom-tool-design-
   manufacture-refurbishment/ as "how images are placed here and it looks nice";
   measured, that page renders 1080x1080 product photographs into 298x298
   frames — aspect MATCHED, so nothing is ever cut — with a generous margin
   around each subject and a uniform label beneath. The ground there is white
   and Shaheen's is a dark plate, which is Shaheen's own language and is not
   being copied; what is adopted is the principle: whole subject, matched or
   contained frame, room around it, labelled. */

/* --------------------------------------------------------------------------
   §W5 · THE LINE-ART HERO ASIDE, AT A STRENGTH THAT READS (2026-08-17)
   Bhavana's note was made about /blog/ — "what is this image for? it does not
   add value" — and /blog/ got the real answer: its slot now promotes the newest
   article (§W3). But the same panel is rendered on SIX other pages (vision,
   gallery, videos, faq, products, privacy, thank-you), and the reason it read
   as nothing is measurable rather than a matter of taste: `.la-field` was set
   to opacity .20 over `--tint`, which puts the linework a couple of percent off
   its own background. At that strength it is not a motif, it is an empty plate.
   Those six pages have no featured post to promote and no published fact worth
   pulling beside the headline — which is exactly why they were given the
   drawing in the first place — so the fix is to make the drawing legible as a
   drawing rather than to swap it for something invented.
   .20 -> .38, and the ink picks up the brand so it reads as the technical
   drawing the brand mandate names, not as a grey smudge.
   The panel is aria-hidden and carries NO text, so there is no contrast pair to
   re-measure; the copy beside it is unaffected, being in a separate column.
   -------------------------------------------------------------------------- */
.p-hero-art .la-field{opacity:.38;color:var(--brand-ink)}

/* ==========================================================================
   §X1 · STATS BANDS — the rules above the numbers are removed (2026-08-17)
   Abhay's note. Every figure on the site sat under a decorative hairline, which
   on a row of stats reads as table chrome rather than as a set of facts. The
   clean pattern is number + label, tight, no rules — the separation comes from
   the gap and from the scale of the numeral, not from a line.
   Removed here rather than by deleting the original declarations, so the rules
   stay findable and reversible and no CSS range is cut by marker span.
   ========================================================================== */
.p-stat{border-top:0;padding-top:0}
.p-block.dark .p-stat{border-top:0}
.pv-c-spec li{border-top:0;padding-top:0}
/* The CTA proof column: the hairline there was acting as an item SEPARATOR, not
   only as decoration, so removing it needs the gap to do that job instead —
   otherwise the facts run together. Measured and set, not assumed. */
.cta-facts li{border-top:0;padding:0}
.cta-facts{display:grid;gap:clamp(16px,1.8vw,24px)}
.cta-facts li:first-child{padding-top:0}

/* --------------------------------------------------------------------------
   §X2 · .p-callout — THE EMPTY-SIDE RULE, APPLIED (2026-08-17)
   Measured at 1440: the warranty band on /rotating-equipment/ and the matching
   band on /certifications/ filled 61% of the container and left ~470px of empty
   BLACK to the right of the card — the standard's empty-side rule broken on the
   single inverted section of each page, which is the most conspicuous place on
   the page to break it.
   Cause: `.p-callout{max-width:70ch}`. The cap is right for a one-column card
   and wrong for a full-width band.
   Resolution — Option C (fill the width by restructuring, not by stretching the
   measure): the card spans the container and sets its statement against its
   body in two columns. The paragraph keeps a ~62ch measure, so nothing is
   harder to read; the band simply stops being half empty.
   Below 900px it returns to a single column, which is the correct phone order.
   -------------------------------------------------------------------------- */
@media (min-width:900px){
  .p-callout{max-width:none;display:grid;grid-template-columns:.85fr 1.15fr;
    gap:clamp(20px,2.4vw,36px);align-items:start;padding:clamp(20px,2vw,28px) clamp(22px,2.4vw,32px)}
  .p-callout .big{align-self:center}
  .p-callout p{margin-top:0;max-width:62ch}
}

/* --------------------------------------------------------------------------
   §X3 · FORM RHYTHM (2026-08-17)
   Measured on /rfq/: a 30px gap between every field, against the standard's
   10-12px. With a label above each input that 30px reads as a gap between
   SECTIONS rather than between fields, which is why the form looks longer than
   it is. Row gap tightened; the COLUMN gap stays wider so the two columns still
   read as two columns and a label never appears to belong to the field beside
   it. Applied to .p-fieldset as well — it carries its own copy of the value,
   and a fix to .p-form alone would have missed every field inside a fieldset,
   which is all of them.
   ⚠ FORM WIDTH IS A DELIBERATE DEVIATION. The standard says 480-520px; this
   form stays 860px in two columns. Narrowing it would undo the 2026-08-02 fix
   that made the RFQ ONE grid with two field widths instead of four, and would
   run a 13-field technical form to roughly twice the height. Single-column
   forms elsewhere are unaffected. Flagged rather than silently ignored.
   -------------------------------------------------------------------------- */
.p-form{gap:14px 28px}
.p-fieldset{gap:14px 28px}
/* ⚠ The group headings need MORE air above them than the fields have between
   them, or the tighter field rhythm makes a new group read as the previous one
   continuing — looked at after the first pass, where "Specification" sat as
   close to "Quantity" as two sibling fields sit to each other. A subsection
   step (the standard's 32-40px band, taken at its lower end because the legend
   already carries its own bottom margin) restores the grouping. */
.p-form .p-fieldset{margin-top:18px}
.p-form .p-fieldset:first-of-type{margin-top:0}
.p-form legend,.p-fieldset legend{margin-bottom:10px}

/* --------------------------------------------------------------------------
   §X4 · NO ORPHAN CELL IN A .p-grid ROW (2026-08-17)
   /capabilities/ lists FIVE disciplines in a three-column grid, so the last row
   is two cards and one empty cell. Same family as the footer's 3-tracks-for-4-
   children defect, the /videos/ auto-fill orphan and the .svc-grid auto-fill
   orphan — all already on file. It is a smaller version, but it is the same
   "content on one side, empty on the other" reading the standard rules out.
   Fixed generically with :nth-last-child, so it self-corrects for ANY count
   rather than being tuned to five: when a 3-col row would end with exactly two
   cards, those two share the row; when it would end with one, that card spans.
   Costs nothing on counts that already divide by three.
   -------------------------------------------------------------------------- */
@media (min-width:1025px){
  /* last row has exactly TWO: they split the full row */
  .p-grid.c3 > :nth-last-child(2):nth-child(3n+1){grid-column:span 2}
  .p-grid.c3 > :last-child:nth-child(3n+2):not(:first-child){grid-column:span 1}
  /* last row has exactly ONE: it spans the remainder rather than sitting alone */
  .p-grid.c3 > :last-child:nth-child(3n+1):not(:first-child){grid-column:span 3}
}

/* ==========================================================================
   §Y2 · THE IMAGE-BACKDROP BAND (2026-08-17)
   A reusable editorial composition, introduced on /vision-2030/ "Why It
   Matters" and available to any section that has one strong photograph and one
   paragraph. The photograph bleeds the full width; a horizontal scrim keeps the
   copy side solid so the type is read against ink, not against a picture, and
   the image resolves on the open side.
   ⚠ THE SCRIM IS THE ACCESSIBILITY DEVICE, not decoration — its stops are set
   from measured contrast over the real photograph, not chosen by eye. Any image
   swap in this band must be re-measured.
   ========================================================================== */
/* ⚠ padding was a bespoke clamp(38,4.4vw,64) that bypassed the rhythm scale, so
   the boundary either side of a backdrop band measured 91px against the 48-56
   the standard sets — the fourth bespoke padding found doing this. On the
   token now, with --sp-lg because a full-bleed band earns the largest step. */
.vbleed{position:relative;overflow:hidden;isolation:isolate;
  padding-block:var(--sp-lg);background:#08090A}
.vbleed-media{position:absolute;inset:0;z-index:0}
.vbleed-media img{width:100%;height:100%;object-fit:cover;object-position:72% center}
/* Two layers doing two jobs: a base darkening so no part of the photograph is
   ever bright enough to fight white text, and the directional fade that gives
   the composition its left-to-right read. */
.vbleed-scrim{position:absolute;inset:0;z-index:1;pointer-events:none;
  background:
    linear-gradient(90deg,rgba(8,9,10,.97) 0%,rgba(8,9,10,.95) 34%,
                          rgba(8,9,10,.78) 54%,rgba(8,9,10,.46) 76%,rgba(8,9,10,.34) 100%),
    linear-gradient(180deg,rgba(8,9,10,.30) 0%,rgba(8,9,10,.12) 40%,rgba(8,9,10,.42) 100%)}
.vbleed-in{position:relative;z-index:2}
.vbleed-copy{max-width:56ch;color:var(--tx-d);
  --brand-text:var(--brand-lift);--brand-graphic:var(--brand-lift)}
/* the heading sits directly above the body it introduces — the whole point of
   the redesign — so the vertical rhythm here is kicker -> heading -> text, at
   the standard's 4-8 / 12-16px, and there is NO separator rule anywhere */
.vbleed-copy .eyebrow{display:block}
.vbleed-copy h2{margin-top:6px;color:#fff}
.vbleed-copy p{margin-top:14px;color:var(--tx-d-dim);max-width:52ch}
.vbleed-cap{position:absolute;right:clamp(14px,2vw,28px);bottom:clamp(12px,1.6vw,20px);
  z-index:3;margin:0;display:flex;align-items:center;gap:8px;
  font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
  font-size:11px;letter-spacing:.12em;text-transform:uppercase;
  color:rgba(255,255,255,.92);background:rgba(8,9,10,.66);
  padding:5px 10px;border-radius:4px}
.vbleed-cap .ph-tag{position:static;background:none;padding:0;font-size:11px;opacity:.9}
@media (max-width:860px){
  /* On a phone the copy is full width, so a LEFT-to-right fade cannot protect
     it — the text would sit over the open side. The fade goes vertical instead
     and the image anchors to its subject. Same failure mode as the hero light
     layer on 2026-08-12: a horizontal mask is meaningless once the copy spans
     the whole width. */
  .vbleed-scrim{background:
    linear-gradient(180deg,rgba(8,9,10,.90) 0%,rgba(8,9,10,.86) 46%,
                           rgba(8,9,10,.72) 74%,rgba(8,9,10,.58) 100%)}
  .vbleed-media img{object-position:60% center}
  .vbleed-copy{max-width:none}
  .vbleed-cap{position:static;margin:18px 0 0;align-self:flex-start;
    display:inline-flex;background:rgba(8,9,10,.5)}
}
@media (prefers-reduced-motion:reduce){ .vbleed *{transition:none!important} }

/* ==========================================================================
   §Y3 · FULL-BLEED PHOTOGRAPHIC HERO (2026-08-17)
   `'bleed' => true` on sacom_page_hero(). Introduced because /about-us/ "did
   not land with impact": it opened with a framed thumbnail beside a paragraph,
   which is the same opening every inner page has. Here the photograph IS the
   scene, the mandate's technical-drawing field sits over it as a design layer,
   and the copy is set on solid ink at the left.
   Same scrim discipline as §Y2: the gradient is an accessibility device with
   measured stops, not decoration. Re-measure on any image swap.
   ========================================================================== */
.p-hero-bleed{position:relative;overflow:hidden;isolation:isolate;
  padding-top:var(--hdr-h);background:#08090A;border-bottom:0;color:var(--tx-d)}
.p-hero-bleed .phb-media{position:absolute;inset:0;z-index:0}
.p-hero-bleed .phb-media img{width:100%;height:100%;object-fit:cover;object-position:64% center}
.p-hero-bleed .phb-scrim{position:absolute;inset:0;z-index:1;pointer-events:none;
  background:
    linear-gradient(90deg,rgba(8,9,10,.96) 0%,rgba(8,9,10,.93) 38%,
                          rgba(8,9,10,.74) 58%,rgba(8,9,10,.44) 80%,rgba(8,9,10,.32) 100%),
    linear-gradient(180deg,rgba(8,9,10,.55) 0%,rgba(8,9,10,.18) 42%,rgba(8,9,10,.55) 100%)}
/* the drawing field as a design layer, not filler — it is the brand mandate's
   third element and it is what makes this read as Shaheen rather than as a
   stock hero. Hairline, brand-lifted, and well under the scrim's headroom. */
.p-hero-bleed .phb-art{position:absolute;inset:0;z-index:2;pointer-events:none;overflow:hidden}
.p-hero-bleed .phb-art .la-field{position:absolute;width:120%;height:140%;
  left:16%;top:-18%;opacity:.20;color:var(--brand-lift)}
.p-hero-bleed .phb-in{position:relative;z-index:3}
.p-hero-bleed .p-hero-grid{grid-template-columns:1fr;
  padding-block:clamp(48px,5.6vw,84px) clamp(52px,6vw,92px)}
.p-hero-bleed .p-hero-copy{max-width:62ch;
  --brand-text:var(--brand-lift);--brand-graphic:var(--brand-lift)}
.p-hero-bleed h1{color:#fff;max-width:19ch}
.p-hero-bleed .lead{color:var(--tx-d-dim);max-width:56ch}
.p-hero-bleed .crumb,.p-hero-bleed .crumb a{color:var(--tx-d-dim)}
.p-hero-bleed .crumb [aria-current]{color:var(--tx-d)}
.p-hero-bleed .cta.ghost{color:#fff}
.phb-cap{position:absolute;right:clamp(14px,2vw,28px);bottom:clamp(12px,1.6vw,20px);
  z-index:4;margin:0;max-width:min(46ch,52vw);text-align:right;
  font-size:.78rem;line-height:1.45;color:rgba(255,255,255,.86);
  background:rgba(8,9,10,.6);padding:8px 12px;border-radius:6px}
.phb-cap b{display:block;font-family:var(--f-dsp);font-weight:600;
  font-size:.86rem;color:#fff;letter-spacing:-.01em}
.phb-cap .ph-tag{position:static;display:inline-block;margin-top:4px}
@media (max-width:860px){
  /* copy spans the width, so the horizontal fade cannot protect it — vertical
     instead, the same correction §Y2 needs and for the same reason */
  .p-hero-bleed .phb-scrim{background:
    linear-gradient(180deg,rgba(8,9,10,.82) 0%,rgba(8,9,10,.88) 45%,rgba(8,9,10,.94) 100%)}
  .p-hero-bleed .phb-art .la-field{left:-10%;width:150%;opacity:.16}
  .p-hero-bleed h1,.p-hero-bleed .lead{max-width:none}
  .phb-cap{position:static;margin:20px 0 0;max-width:none;text-align:left;
    display:inline-block;background:rgba(8,9,10,.45)}
}

/* ==========================================================================
   §Y4 · THE PROCESS SEQUENCE (2026-08-17)
   Steps scroll on the left; a dial holds on the right and advances with them.
   NATIVE STICKY — no pin, no scroll-jack, nothing takes the scroll. See the
   note on sacom_process_seq() for why that matters here.
   ========================================================================== */
.pseq{margin-top:clamp(18px,2vw,28px)}
.pseq-grid{display:grid;grid-template-columns:1fr;gap:clamp(20px,2.4vw,34px)}
.pseq-steps{list-style:none;margin:0;padding:0;display:grid;gap:clamp(14px,1.6vw,22px)}
.pseq-step{display:grid;grid-template-columns:auto 1fr;gap:14px 18px;align-items:start;
  padding:clamp(16px,1.8vw,24px);border-radius:var(--r);background:var(--white);
  border:1px solid var(--hair-l)}
.pseq-n{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
  font-size:.72rem;letter-spacing:.12em;color:var(--brand-ink);padding-top:3px}
.pseq-step h3{grid-column:2;margin:0}
.pseq-step p{grid-column:2;margin-top:8px;color:var(--tx-l-dim);max-width:56ch}
.pseq-rail{display:none}

@media (min-width:1000px){
  .pseq-grid{grid-template-columns:1.25fr .75fr;gap:clamp(30px,3.4vw,56px);align-items:start}
  .pseq-rail{display:block;position:sticky;top:calc(var(--hdr-h) + 24px);align-self:start}
  .pseq-viz{border-radius:var(--r-lg);padding:clamp(20px,2.2vw,30px);
    background:radial-gradient(90% 90% at 50% 10%,#151a18 0%,#0f1114 72%);
    color:var(--tx-d);text-align:center}
  .pseq-dial{position:relative;width:100%;max-width:220px;margin:0 auto;aspect-ratio:1}
  .pseq-ring{width:100%;height:100%;display:block;transform:rotate(-90deg)}
  .pseq-ring circle{fill:none;stroke-width:3;vector-effect:non-scaling-stroke}
  .pseq-track{stroke:rgba(255,255,255,.14)}
  .pseq-prog{stroke:var(--brand-lift);stroke-dasharray:1;stroke-dashoffset:1;
    stroke-linecap:round;transition:stroke-dashoffset .35s var(--ease)}
  .pseq-tick{stroke:rgba(255,255,255,.22);stroke-width:2;vector-effect:non-scaling-stroke;
    transition:stroke .3s var(--ease)}
  .pseq-tick.on{stroke:var(--brand-lift)}
  .pseq-cur{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
    font-family:var(--f-dsp);font-weight:600;font-size:clamp(2rem,3.2vw,2.75rem);
    letter-spacing:-.02em;color:#fff;font-variant-numeric:tabular-nums;
    transform:translateY(-6px)}
  .pseq-of{position:absolute;left:0;right:0;bottom:22%;
    font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
    font-size:11px;letter-spacing:.14em;color:rgba(255,255,255,.55)}
  .pseq-name{margin-top:14px;font-family:var(--f-dsp);font-weight:600;
    font-size:1.02rem;line-height:1.3;color:#fff;min-height:2.6em;
    display:flex;align-items:center;justify-content:center;
    transition:opacity .25s var(--ease)}
  .pseq-lab{margin-top:2px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
    font-size:11px;letter-spacing:.16em;text-transform:uppercase;color:var(--brand-lift)}
  /* the active step lifts out of the list so the left and right agree */
  .pseq-step{transition:border-color .3s var(--ease),background .3s var(--ease),
    transform .3s var(--ease)}
  .js .pseq-step{opacity:.62;transition:opacity .3s var(--ease),border-color .3s var(--ease),
    background .3s var(--ease),transform .3s var(--ease)}
  .js .pseq-step.on{opacity:1;border-color:rgba(4,105,56,.30);
    background:var(--white);transform:translateX(4px)}
}
@media (prefers-reduced-motion:reduce){
  /* static, complete, and every step at full strength — nothing depends on scroll */
  .js .pseq-step,.pseq-step{opacity:1!important;transform:none!important;transition:none!important}
  .pseq-prog{stroke-dashoffset:0!important;transition:none!important}
  .pseq-tick{stroke:var(--brand-lift)!important}
  .pseq-name,.pseq-cur{transition:none!important}
}

/* ==========================================================================
   §Y5 · CONTACT — DESIGNED BLOCKS AND A COMPOSED MAP (2026-08-17)
   Abhay: "plain, no imagery, redundant details". The nine-row definition list
   is now four cards grouped by INTENT — visit, call, write, when — each with a
   mono key, its detail, and the one action a reader wants from it. The three
   reference facts that were rows of their own (company name, website, CR No.)
   are demoted to a single legal line rather than deleted.
   ========================================================================== */
/* B5b · the contact blocks read as four defined cards, two up, with the
   label, the detail and the action on a clear vertical rhythm. The green
   left spine they used to carry is gone under the no-accent-line rule —
   definition now comes from the tint ground, the hairline and the padding. */
.cc-cards{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));
  gap:clamp(12px,1.3vw,16px);margin-top:18px}
@media (max-width:860px){.cc-cards{grid-template-columns:1fr}}
@media (min-width:560px){ .cc-cards{grid-template-columns:1fr 1fr} }
.cc-card{position:relative;padding:clamp(16px,1.8vw,22px);border-radius:var(--r);
  background:var(--tint);border:1px solid var(--hair-l);
  transition:border-color .3s var(--ease),transform .3s var(--ease)}
.cc-card:hover{border-color:rgba(4,105,56,.28);transform:translateY(-2px)}
/* the brand's edge device, on the axis .svc-media and .prod-media already use */
.cc-card::before{display:none}
.cc-k{display:block;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
  font-size:11px;letter-spacing:.16em;text-transform:uppercase;color:var(--brand-ink)}
.cc-body{margin-top:12px;font-size:.95rem;line-height:1.6;color:var(--tx-l-dim)}
.cc-body a{display:block;color:var(--tx-l);font-weight:600;text-decoration:none;
  padding:2px 0;min-height:26px}
.cc-body a:hover{color:var(--brand-ink)}
.cc-sub{display:block;margin:2px 0 10px;font-size:.76rem;color:var(--tx-l-dim);font-weight:400}
.cc-sub:last-child{margin-bottom:0}
.cc-act{display:inline-flex;align-items:center;gap:8px;margin-top:12px;
  font-size:.82rem;font-weight:600;color:var(--brand-ink);text-decoration:none;
  min-height:30px}
.cc-act svg{width:14px;height:14px;transition:transform .25s var(--ease)}
.cc-act:hover svg{transform:translateX(3px)}
.cc-legal{margin-top:14px;font-size:.76rem;color:var(--tx-l-dim)}
/* The address block carries the most text, so it takes the full row rather than
   setting a 30-word address in a half-width card beside a two-line one. */
@media (min-width:560px){ .cc-card.cc-visit{grid-column:1/-1} }

/* ---- the composed location band ---- */
.cmap-frame{position:relative;border-radius:var(--r-lg);overflow:hidden;
  background:#0f1114;margin-top:clamp(16px,1.8vw,24px)}
.cmap-frame iframe{display:block;width:100%;height:clamp(280px,34vw,440px);border:0;
  filter:grayscale(.32) contrast(1.04)}
.cmap-card{position:absolute;left:clamp(12px,1.8vw,24px);bottom:clamp(12px,1.8vw,24px);
  z-index:2;max-width:min(34ch,74%);padding:clamp(14px,1.6vw,20px);
  border-radius:var(--r);background:rgba(255,255,255,.96);
  box-shadow:0 18px 44px -22px rgba(8,9,10,.45)}
.cmap-card p{margin-top:8px;font-size:.92rem;line-height:1.5;color:var(--tx-l)}
@media (max-width:560px){
  .cmap-card{position:static;max-width:none;border-radius:0;box-shadow:none;
    background:var(--tint)}
  .cmap-frame{background:none}
}

/* ==========================================================================
   §Y6 · THE HEAD/INTRO PAIR BECOMES A ROW, NOT A LEFT COLUMN (2026-08-17)
   Abhay's screenshot: "Where these components run." — a section head and its
   intro paragraph stacked in the left half, the entire right half empty, then
   a card grid underneath. The section reads as a broken two-column.
   Nothing needs to be invented to fill it: the intro paragraph is ALREADY
   there, it was simply set below the heading instead of beside it. The heading
   takes the left column, the intro takes the right, and everything after them
   spans the full width as before.
   CSS-only and pattern-scoped with :has(), so it applies wherever the shape
   occurs — three sections today (/rotating-equipment/, /cutting-tools/,
   /capabilities/) — and to any that adopt it later, with no markup change.
   ========================================================================== */
@media (min-width:1000px){
  .p-block .wrap:has(> .p-head + .prose){
    display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.12fr);
    column-gap:clamp(30px,3.4vw,56px);align-items:start}
  /* everything spans the full width by default; only the pair is placed */
  .p-block .wrap:has(> .p-head + .prose) > *{grid-column:1/-1}
  .p-block .wrap:has(> .p-head + .prose) > .p-head:has(+ .prose){grid-column:1}
  .p-block .wrap:has(> .p-head + .prose) > .p-head + .prose{grid-column:2;
    margin-top:0!important;max-width:none}
  .p-block .wrap:has(> .p-head + .prose) > .p-head + .prose p{max-width:58ch}
}

/* ==========================================================================
   §Z1 · SPECIALISED PRODUCTS — the catalogue opens on a chooser (2026-08-17)
   Four identical text pills in front of a 36-item catalogue give a buyer no way
   in. The categories become tiles carrying a photograph and an item count, so
   the first decision is made visually. They keep the .cat-btn / data-filter
   hooks, so the existing handler drives them and no JS was added.
   ========================================================================== */
.catbento{display:grid;grid-template-columns:1fr 1fr;gap:clamp(10px,1.2vw,16px);
  flex-wrap:wrap;overflow:visible}
@media (min-width:900px){ .catbento{grid-template-columns:repeat(4,1fr)} }
.cat-btn.cbx{display:block;padding:0;background:none;border:1px solid var(--hair-l);
  border-radius:var(--r);overflow:hidden;text-align:left;cursor:pointer;white-space:normal;
  transition:border-color .3s var(--ease),transform .3s var(--ease),box-shadow .3s var(--ease)}
.p-block.alt .cat-btn.cbx{background:var(--white)}
.cat-btn.cbx:hover{border-color:rgba(4,105,56,.32);transform:translateY(-3px);
  box-shadow:0 18px 40px -24px rgba(8,9,10,.34)}
.cat-btn.cbx:focus-visible{outline:2px solid var(--brand-graphic);outline-offset:3px}
.cbx-media{display:block;position:relative;aspect-ratio:4/3;overflow:hidden;background:#0f1114}
.cbx-media img{width:100%;height:100%;object-fit:contain;padding:6px;
  transition:transform .5s var(--ease)}
.cat-btn.cbx:hover .cbx-media img{transform:scale(1.05)}
.cbx-media::after{display:none}
.cbx-body{display:block;padding:12px 14px 14px}
.cbx-n{display:block;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
  font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--brand-ink)}
.cbx-l{display:block;margin-top:6px;font-family:var(--f-dsp);font-weight:600;
  font-size:.94rem;line-height:1.3;letter-spacing:-.012em;color:var(--tx-l)}
/* the ACTIVE category reads as chosen, not merely hovered */
.cat-btn.cbx.on{border-color:var(--brand);box-shadow:0 0 0 1px var(--brand) inset;background:var(--tint)}
.p-block.alt .cat-btn.cbx.on{background:var(--tint)}
.cat-btn.cbx.on .cbx-l{color:var(--brand-ink)}
/* the reset sits apart from the four, as a control rather than a fifth category */
.cat-btn.cbx-all{grid-column:1/-1;justify-self:start;margin-top:2px}

/* ---- typographic tiles for catalogue items with no photograph yet ----
   Fifteen picture-glyph plates on one page read as fifteen broken images. An
   unphotographed catalogue entry is not a broken image, so it is set as type
   and says exactly what it is. */
.prod-type{position:absolute;inset:0;display:flex;flex-direction:column;
  align-items:flex-start;justify-content:flex-end;gap:8px;
  padding:clamp(12px,1.4vw,18px);
  background:radial-gradient(120% 100% at 12% 0%,#1b201d 0%,#121614 58%,#0e1110 100%)}
.prod-type-n{font-family:var(--f-dsp);font-weight:600;line-height:1.18;
  font-size:clamp(1rem,1.3vw,1.2rem);letter-spacing:-.015em;color:rgba(255,255,255,.94)}
.prod-type-k{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
  font-size:11px;letter-spacing:.14em;text-transform:uppercase;
  color:rgba(255,255,255,.62)}
/* the brand edge returns on these tiles — they are catalogue entries like any
   other, and hiding the rule was what made them read as "missing" */
.prod.no-img .prod-media::after{display:block}
/* cut-outs keep `contain` (cover renders them as black plates — 2026-07-28), but
   14px of padding on a dark plate is the padded-box look, so it is trimmed */
.prod-media img{padding:8px}

/* ==========================================================================
   §Z2 · CERTIFICATIONS — the marks composed as a wall (2026-08-17)
   Three standards were three plain text cells on the page whose entire job is
   credibility. The standard NUMBER now carries at display scale with the system
   it governs beneath it, and the mandate's arc sits behind each mark so the
   three read as a set. No new copy.
   ========================================================================== */
.certwall{display:grid;grid-template-columns:1fr;gap:clamp(12px,1.4vw,18px)}
@media (min-width:760px){ .certwall{grid-template-columns:repeat(3,1fr)} }
.certmark{position:relative;overflow:hidden;isolation:isolate;
  padding:clamp(22px,2.4vw,32px);border-radius:var(--r);
  background:radial-gradient(120% 110% at 14% 0%,#161b18 0%,#101413 60%,#0d100f 100%);
  color:var(--tx-d)}
/* the technical-drawing arc, drawn in CSS so it costs no request and cannot
   fail to load — concentric rings bleeding off the top-right corner */
.certmark-art{position:absolute;right:-28%;top:-46%;width:78%;aspect-ratio:1;z-index:0;
  border-radius:50%;pointer-events:none;
  border:1px solid rgba(6,167,89,.30);
  box-shadow:0 0 0 14px rgba(6,167,89,.10),0 0 0 30px rgba(6,167,89,.06),
             0 0 0 50px rgba(6,167,89,.035)}
.certmark-n{position:relative;z-index:1;display:block;font-family:var(--f-dsp);
  font-weight:600;line-height:1;letter-spacing:-.028em;
  font-size:clamp(1.6rem,2.6vw,2.15rem);color:#fff;font-variant-numeric:tabular-nums}
.certmark-s{position:relative;z-index:1;display:block;margin-top:12px;
  font-size:.9rem;line-height:1.45;color:var(--tx-d-dim);max-width:26ch}

/* ---- the verification checklist, as marks rather than a run-on sentence ---- */
.qchecks{list-style:none;margin:clamp(16px,1.8vw,24px) 0 0;padding:0;
  display:flex;flex-wrap:wrap;gap:8px}
.qchecks li{display:inline-flex;align-items:center;gap:8px;
  padding:7px 13px;border-radius:999px;background:var(--white);
  border:1px solid var(--hair-l);font-size:.82rem;color:var(--tx-l)}
.p-block:not(.alt) .qchecks li{background:var(--tint)}
.qchecks li::before{content:"";width:6px;height:6px;flex:none;border-radius:50%;
  background:var(--brand-graphic)}

/* ==========================================================================
   §Z3 · FAQ — STICKY CATEGORY RAIL (2026-08-17)
   Was one full-width section per category: five identical heads stacked down
   the page, ~430px of empty space beside every accordion, and no sense of where
   you were. Now one section, a sticky rail that lists the categories once and
   marks the one you are reading, and the accordions beside it.
   Native sticky. No pin, nothing takes the scroll, and the rail is plain anchor
   links so it navigates with JS disabled.
   ========================================================================== */
.faqx-grid{display:grid;grid-template-columns:1fr;gap:clamp(20px,2.4vw,34px)}
.faqx-rail{min-width:0}
.faqx-k{display:block;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
  font-size:11px;letter-spacing:.16em;text-transform:uppercase;color:var(--brand-ink);
  margin-bottom:10px}
.faqx-rail ul{list-style:none;margin:0;padding:0;display:flex;flex-wrap:wrap;gap:6px}
.faqx-rail a{display:inline-flex;align-items:center;gap:10px;text-decoration:none;
  padding:9px 13px;border-radius:999px;background:var(--tint);
  border:1px solid var(--hair-l);color:var(--tx-l-dim);font-size:.85rem;
  min-height:38px;transition:color .25s var(--ease),border-color .25s var(--ease),
  background .25s var(--ease)}
.faqx-rail a:hover{color:var(--tx-l);border-color:rgba(4,105,56,.28)}
.faqx-rail a.on{color:var(--tx-l);font-weight:600;border-color:var(--brand);
  background:var(--white);box-shadow:0 0 0 1px var(--brand) inset}
.faqx-c{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
  font-size:11px;letter-spacing:.1em;color:var(--brand-ink)}
.faqx-ask{display:none}
.faqx-h{font-family:var(--f-dsp);font-weight:600;letter-spacing:-.02em;
  font-size:clamp(1.25rem,1.9vw,1.6rem);color:var(--tx-l);margin-bottom:14px}
.faqx-group + .faqx-group{margin-top:clamp(26px,3vw,42px)}
/* an anchor jump must not land the heading under the fixed header */
.faqx-group{scroll-margin-top:calc(var(--hdr-h) + 20px)}

@media (min-width:1000px){
  .faqx-grid{grid-template-columns:.3fr .7fr;gap:clamp(34px,4vw,64px);align-items:start}
  .faqx-rail{position:sticky;top:calc(var(--hdr-h) + 24px);align-self:start}
  .faqx-rail ul{flex-direction:column;flex-wrap:nowrap;gap:2px}
  .faqx-rail a{width:100%;justify-content:space-between;border-radius:var(--r-sm);
    background:none;border:0;border-left:2px solid transparent;padding:10px 12px}
  .faqx-rail a:hover{background:var(--tint);border-color:transparent}
  .faqx-rail a.on{background:var(--tint);border-left-color:var(--brand);
    box-shadow:none;border-radius:0 var(--r-sm) var(--r-sm) 0}
  /* the rail has room for the one action a reader wants when nothing fits */
  .faqx-ask{display:inline-flex;align-items:center;gap:8px;margin-top:20px;
    padding-top:16px;border-top:1px solid var(--hair-l);
    font-size:.84rem;font-weight:600;color:var(--brand-ink);text-decoration:none}
  .faqx-ask svg{width:14px;height:14px;transition:transform .25s var(--ease)}
  .faqx-ask:hover svg{transform:translateX(3px)}
  /* the answer keeps its measure; the BAR uses the column (the 2026-08-02 fix) */
  .faqx-body .faq-list{max-width:none}
  .faqx-body .faq-a p{max-width:72ch}
}
@media (prefers-reduced-motion:reduce){ .faqx-rail a,.faqx-ask svg{transition:none!important} }

/* ==========================================================================
   §Z4 · VIDEOS · BLOG · RFQ (2026-08-17)
   ========================================================================== */

/* ---- VIDEOS: the one real film leads its category; the rest say what they are.
   Ten identical grey plates with a generic glyph read as ten broken tiles. There
   is no film in the repository and that is a fact about our footage, not a
   missing asset, so each tile states it. ---- */
.vid-type{position:absolute;inset:0;display:flex;flex-direction:column;
  align-items:flex-start;justify-content:flex-end;gap:8px;
  padding:clamp(14px,1.6vw,20px);
  background:radial-gradient(120% 100% at 14% 0%,#1b201d 0%,#121614 58%,#0e1110 100%)}
.vid-type-i{position:absolute;right:clamp(14px,1.6vw,20px);top:clamp(14px,1.6vw,20px);
  width:26px;height:26px;color:var(--brand-lift);opacity:.85}
.vid-type-n{font-family:var(--f-dsp);font-weight:600;line-height:1.2;
  font-size:clamp(.98rem,1.2vw,1.14rem);letter-spacing:-.015em;color:rgba(255,255,255,.94);
  max-width:22ch}
.vid-type-k{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
  font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:rgba(255,255,255,.64)}
/* the ONE real film is the feature of its own category — the doc's category
   ORDER is untouched, the tile simply takes the room it has earned */
@media (min-width:900px){
  .vid-grid{grid-template-columns:repeat(3,minmax(0,1fr))}
  .vid.is-film{grid-column:span 2}
}

/* ---- BLOG: the newest article leads ---- */
@media (min-width:900px){
  .blog-grid.is-featured{grid-template-columns:repeat(2,minmax(0,1fr))}
  .bcard-lead{grid-column:1/-1;display:grid;grid-template-columns:1.15fr .85fr;
    align-items:stretch}
  /* ⚠ height:100% against an auto-height row let the media take the image's
     natural height — the lead card measured 675px tall. A real aspect box caps
     it and keeps CLS at zero. */
  .bcard-lead .bcard-media{aspect-ratio:16/11;height:auto;min-height:0}
  .bcard-lead .bcard-body{align-self:center;padding:clamp(20px,2.4vw,34px)}
  .bcard-lead h3{font-size:clamp(1.25rem,1.9vw,1.6rem);line-height:1.24}
  .bcard-lead p{max-width:52ch}
}
@media (min-width:1180px){ .blog-grid.is-featured{grid-template-columns:repeat(3,minmax(0,1fr))} }

/* ---- RFQ: a stage rail so a 13-field form declares its length ---- */
.rprog{list-style:none;margin:0 0 clamp(16px,1.8vw,24px);padding:0;
  display:flex;flex-wrap:wrap;gap:6px;grid-column:1/-1}
.rprog li{display:inline-flex;align-items:center;gap:9px;padding:8px 14px;
  border-radius:999px;background:var(--tint);border:1px solid var(--hair-l);
  font-size:.82rem;color:var(--tx-l-dim);
  transition:color .3s var(--ease),border-color .3s var(--ease),background .3s var(--ease)}
.rprog-n{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
  font-size:11px;letter-spacing:.12em;color:var(--brand-ink)}
.rprog li.on{color:var(--tx-l);font-weight:600;background:var(--white);
  border-color:var(--brand);box-shadow:0 0 0 1px var(--brand) inset}
.rprog li.done{color:var(--tx-l)}
.rprog li.done .rprog-n{opacity:.6}
@media (min-width:900px){
  /* it holds while the form scrolls past — native sticky, no pin */
  /* ⚠ This was `background:var(--bg)` — A TOKEN THAT DOES Not EXIST. An invalid
     custom property makes the declaration invalid at computed-value time, so the
     background fell back to transparent and the sticky rail scrolled OVER the
     form fields with the inputs showing through it. Caught by looking at the
     render, not by reading the CSS; `var()` of an undefined token fails silently
     and the rule still appears correct in the file.
     Same bug was in .pseq-step, .pseq-step.on and .hl-card — all four fixed. */
  .rprog{position:sticky;top:calc(var(--hdr-h) + 10px);z-index:3;
    background:var(--white);margin-inline:calc(var(--gutter) * -0.5);
    padding:10px clamp(10px,1.2vw,18px);border-bottom:1px solid var(--hair-l)}
  .p-block.alt .rprog{background:var(--tint)}
}
@media (prefers-reduced-motion:reduce){ .rprog li{transition:none!important} }

/* The new mono labels were set at .60-.64rem, i.e. 9.6-10.2px — under the 11px
   small-text floor the 2026-08-12 mobile pass established and under the ATF
   standard's 11-12px kicker band. Set in px, not rem, so a root-size change
   cannot drift them back below the floor. */

/* ==========================================================================
   §G · GALLERY — A MASONRY SHOWCASE (2026-08-17, C3)
   The grid was uniform small tiles and the page read flat. It becomes a
   varied-height masonry with the images considerably larger, so the work is
   actually looked at rather than skimmed.
   ⚠ CSS COLUMNS, not grid. A masonry needs varied heights flowing around each
   other; `grid` would force a row height and reintroduce the uniform look, and
   the real CSS `masonry` value is not shipping broadly enough to rely on. The
   trade-off is column-order reading, which is correct here — a gallery is
   browsed, not read in sequence.
   ========================================================================== */
@media (min-width:700px){
  .gal-grid.is-masonry{display:block;columns:2;column-gap:clamp(14px,1.6vw,22px)}
}
@media (min-width:1100px){ .gal-grid.is-masonry{columns:3} }
.gal-grid.is-masonry .gal-item{
  break-inside:avoid;-webkit-column-break-inside:avoid;page-break-inside:avoid;
  display:block;width:100%;margin:0 0 clamp(14px,1.6vw,22px)}
/* the varied sizing that makes it a masonry rather than a column of equals */
.gal-grid.is-masonry .gal-shot{aspect-ratio:4/3}
.gal-grid.is-masonry .is-feat .gal-shot{aspect-ratio:4/5}
.gal-grid.is-masonry .gal-item:nth-child(5n+3) .gal-shot{aspect-ratio:1/1}
.gal-grid.is-masonry .gal-item:nth-child(7n+5) .gal-shot{aspect-ratio:16/10}
/* the transition, on the whole tile */
.gal-grid.is-masonry .gal-shot{overflow:hidden;border-radius:var(--r);
  transition:transform .5s var(--ease),box-shadow .5s var(--ease)}
.gal-grid.is-masonry .gal-item:hover .gal-shot{transform:translateY(-5px);
  box-shadow:0 26px 56px -28px rgba(8,9,10,.55)}
.gal-grid.is-masonry .gal-shot img{transition:transform .7s var(--ease)}
.gal-grid.is-masonry .gal-item:hover .gal-shot img{transform:scale(1.05)}
/* the caption resolves on hover instead of sitting under every tile — the
   images carry the page, the caption confirms what you are looking at */
.gal-grid.is-masonry .eframe-cap{margin-top:10px}
.gal-grid.is-masonry .gal-item:focus-visible .gal-shot{outline:2px solid var(--brand-graphic);outline-offset:3px}
@media (prefers-reduced-motion:reduce){
  .gal-grid.is-masonry .gal-shot,.gal-grid.is-masonry .gal-shot img{transition:none!important}
  .gal-grid.is-masonry .gal-item:hover .gal-shot{transform:none}
  .gal-grid.is-masonry .gal-item:hover .gal-shot img{transform:none}
}

/* ==========================================================================
   §R · RFQ — THE STEPPED STRUCTURE, DESIGNED (2026-08-17, C4)
   Measured first: the grid itself is CORRECT — exactly two field widths
   (416/860) and two left edges (0/444), so the 2026-08-02 one-grid fix is
   holding and there are no mid-form column jumps. Nothing about the columns
   needed changing, and changing them would have undone that fix.
   What reads as "floating" is two other things:
     · the Specification step runs two 2-column rows and then drops to a
       FULL-WIDTH Application field, so the eye loses the grid at that point;
     · the legends carry no more weight than a field label, so the three steps
       never read as steps.
   Both are fixed by making the step structure explicit rather than by moving
   fields: each fieldset becomes a defined block with a numbered header, and the
   full-width tail of a step is grouped so the width change reads as intentional.
   Numbers come from a CSS counter — no copy is added to the markup.
   ========================================================================== */
.p-form{counter-reset:rfqstep}
.p-form .p-fieldset{counter-increment:rfqstep;position:relative;
  padding-top:clamp(18px,2vw,26px);border-top:1px solid var(--hair-l)}
.p-form .p-fieldset:first-of-type{border-top:0;padding-top:0}
.p-form legend{display:flex;align-items:baseline;gap:10px;float:left;width:100%;
  font-family:var(--f-dsp);font-weight:600;letter-spacing:-.015em;
  font-size:clamp(1.02rem,1.3vw,1.18rem);color:var(--tx-l)}
.p-form legend::before{content:counter(rfqstep,decimal-leading-zero);
  font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
  font-size:11px;letter-spacing:.12em;color:var(--brand-ink);font-weight:400}
/* the full-width tail of a step (application, upload, message) is grouped, so
   the drop from two columns to one reads as a deliberate change of gear */
.p-form .field:has(textarea),.p-form .field:has(input[type=file]){
  grid-column:1/-1}
@media (min-width:900px){
  .p-form .p-fieldset > .field[style*="1/-1"],
  .p-form .field:has(textarea),
  .p-form .field:has(input[type=file]){
    padding-top:clamp(6px,0.8vw,10px);
    border-top:1px dashed var(--hair-l);margin-top:clamp(4px,0.6vw,8px)}
  .p-form .p-fieldset .field:has(textarea) + .field:has(textarea){
    border-top:0;margin-top:0;padding-top:0}
}

/* ==========================================================================
   §H · THE SECTION HEAD WITH A LEAD BECOMES A ROW (2026-08-17, C1)
   Abhay's example: "White metal processes / How the Babbitt goes on." — the
   heading and three lines of intro stacked in the left 45%, an empty right
   half, and then four tiles underneath.
   ⚠ §Y6 could not catch this. That rule matches `.p-head + .prose`, i.e. an
   intro that is a SIBLING of the head. Here the intro is the third argument to
   sacom_section_head() and is rendered as `<p class="lead">` INSIDE `.p-head`,
   so the selector never applied. Same defect, different markup — found by
   reading the call, not by re-running the detector.
   Nothing is invented to fill the space: the intro already exists, it was just
   set below the heading instead of beside it. Heading left, intro right, then
   the grid full width beneath — the same move, now for the inline lead.
   ⚠ Scoped to heads that ACTUALLY carry a lead. A head without one is a
   heading above a full-width grid, which is correct and must not be touched.
   ========================================================================== */
@media (min-width:1000px){
  /* ⚠ `.p-head{max-width:64ch}` is correct for a stacked head and WRONG for a
     two-column one — it capped the whole row at 560px of a 1200 container and
     left ~590px empty to the right, i.e. the same defect one level in. Measured
     after the first attempt, not assumed. The cap is released HERE ONLY; every
     head without a lead keeps its 64ch measure. */
  .p-block .p-head:has(> .lead){
    max-width:none;
    display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.1fr);
    column-gap:clamp(30px,3.4vw,56px);align-items:end}
  .p-block .p-head:has(> .lead) > h2{max-width:18ch}
  .p-block .p-head:has(> .lead) > .la-accent{grid-column:1;grid-row:1}
  .p-block .p-head:has(> .lead) > .eyebrow{grid-column:1;align-self:end}
  .p-block .p-head:has(> .lead) > h2{grid-column:1;margin-top:6px}
  .p-block .p-head:has(> .lead) > .lead{grid-column:2;grid-row:2/4;
    align-self:end;margin-top:0;max-width:56ch}
}
/* the process/spec tiles gain a light reveal, in the homepage's language —
   an affordance, not decoration, and it costs one property */
.p-cell{transition:border-color .3s var(--ease),transform .3s var(--ease),
  box-shadow .3s var(--ease)}
.p-cell:hover{transform:translateY(-3px);
  box-shadow:0 18px 40px -26px rgba(8,9,10,.30)}
@media (prefers-reduced-motion:reduce){
  .p-cell{transition:none!important}
  .p-cell:hover{transform:none;box-shadow:none}
}

/* ============================================================
   §M-TAP · mobile tap targets on THIS SPRINT'S NEW COMPONENTS
   ------------------------------------------------------------
   ⚠ Found by the final 390 sweep, and it is my own regression.
   The 2026-08-16 pass drove tap targets under 44px to ZERO at
   390 — but every rule it wrote was scoped to the elements that
   existed THEN. `.cc-body a`, `.cc-act`, `.faqx-rail a` and
   `.faqx-ask` are markup this sprint introduced, so none of
   those rules could reach them and the guarantee silently
   lapsed: contact measured 11 targets under 44px, faq 5.

   This is the SAME class of miss recorded twice already — a
   class-scoped fix cannot cover markup added after it. The rule
   that follows from it: after adding any new link or button
   component, re-run the 390 tap sweep, because a passing sweep
   from a previous sprint says nothing about new elements.

   Padding only, so desktop geometry is untouched. The inline
   PROSE links (`.p-note` "RFQ form", the consent line's
   "Privacy Policy") are deliberately left alone — WCAG 2.2
   exempts links inline in a sentence, and boxing them breaks
   the line. That is the same call made on 2026-08-16.
   ============================================================ */
@media (max-width:1000px){
  /* contact directory — phone numbers and the email address */
  .cc-body a{min-height:44px;display:flex;align-items:center;padding:4px 0}
  /* "Get directions" / "Open in Maps" / "Message on WhatsApp" / LinkedIn */
  .cc-act{min-height:44px;padding-block:8px}
  /* faq category rail, in its wrapped-pill (mobile) form */
  .faqx-rail a{min-height:44px;padding-block:11px}
  .faqx-ask{min-height:44px;padding-bottom:6px}
}

/* ============================================================
   §GS · GALLERY — per-category slider (B4, 2026-08-18)
   Replaces the masonry. Native scroll-snap: swipe/momentum come
   from the platform, it degrades to a plain scrollable row with
   no JS, and the fixed-aspect stage keeps CLS at 0.
   No accent lines (§NA) — definition is ground + caption + scale.
   ============================================================ */
.gslider{margin-top:18px}
.gs-track{display:flex;gap:clamp(12px,1.4vw,20px);overflow-x:auto;
  scroll-snap-type:x mandatory;scroll-behavior:smooth;
  padding-bottom:6px;scrollbar-width:thin;
  -webkit-overflow-scrolling:touch}
.gs-track::-webkit-scrollbar{height:6px}
.gs-track::-webkit-scrollbar-thumb{background:var(--hair-l);border-radius:99px}
.gs-track:focus-visible{outline:2px solid var(--brand-graphic);outline-offset:3px}

/* THE STAGE IS LARGE — that was the complaint. One slide fills most of the
   column on desktop and effectively all of it on a phone. */
.gs-slide{flex:0 0 auto;width:var(--gsw);scroll-snap-align:start;
  display:block;text-align:left;padding:0;border:0;background:none;
  cursor:zoom-in;font:inherit;color:inherit}
.gs-slide.is-ph{cursor:default}
.gs-shot{display:block;position:relative;aspect-ratio:4/3;overflow:hidden;
  border-radius:var(--r);background:#0f1114}
.gs-shot img{width:100%;height:100%;object-fit:cover;display:block;
  transition:transform .5s var(--ease)}
/* PART C · the gallery slider's contained frames take the SAME light product
   ground as `.eframe.contain`, so a part looks identical wherever it appears.
   The blurred-self ground it used before is retired here for the same reason
   trial B lost: it is still a dark ground. */
.gs-shot.contain{background:var(--shot-ground)}
.gs-shot.contain img{position:relative;object-fit:contain;padding:0}
.gs-slide:hover .gs-shot img{transform:scale(1.03)}
.gs-slide .eframe-cap{margin-top:10px}

.gs-nav{display:flex;align-items:center;justify-content:space-between;
  gap:16px;margin-top:12px}
.gs-count{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
  font-size:11px;letter-spacing:.12em;color:var(--tx-l-dim)}
.gs-btns{display:flex;gap:8px}
.gs-b{width:44px;height:44px;border-radius:999px;cursor:pointer;
  border:1px solid var(--hair-l);background:var(--white);color:var(--tx-l);
  font-size:1rem;line-height:1;transition:border-color .25s var(--ease),
  background .25s var(--ease),opacity .25s var(--ease)}
.gs-b:hover{border-color:rgba(4,105,56,.34);background:var(--tint)}
.gs-b[disabled]{opacity:.32;cursor:default}
.gs-b:focus-visible{outline:2px solid var(--brand-graphic);outline-offset:2px}

@media (max-width:860px){ .gs-nav{margin-top:10px} }

@media (prefers-reduced-motion:reduce){
  .gs-track{scroll-behavior:auto}
  .gs-shot img,.gs-b{transition:none!important}
  .gs-slide:hover .gs-shot img{transform:none}
}

/* ============================================================
   §PVS · COMPONENTS — sticky stage, scrolling list  (B1, 2026-08-18)
   The /about-us/ "Our Approach" pattern applied here: the schematic
   panel HOLDS while the long component list travels past it, then
   the section releases normally.

   NATIVE position:sticky — NO pin is added. CLAUDE.md records the
   carousel and the gear as the second AND FINAL approved pin
   exceptions; a sticky column never takes the scroll, so the page
   stays escapable and this needs no exception.

   Desktop only (>=900px). Below that the grid is already one
   column: the list and panels stack and nothing sticks.
   ============================================================ */
@media (min-width:900px){
  .pv-a-grid{align-items:start}
  .pv-a-stage{position:sticky;top:calc(var(--hdr-h) + 26px);align-self:start}
  /* the list must be TALLER than the stage or there is nothing to hold
     through — the rows open up so the travel is real, not simulated */
  .pv-a-list .pv-a-item{padding-block:21px}
}
@media (prefers-reduced-motion:reduce){
  .pv-a-stage{position:static}
}

/* ============================================================
   §B3 · SECTION HEAD BESIDE PROSE — share a top text axis
   ------------------------------------------------------------
   Abhay: "the left headline sits at the bottom of its column vs
   the right content." Measured on /rotating-equipment/
   "Application coverage": the .p-head BOX and the .prose both
   start at y=2892, but the head's first text (the eyebrow) does
   not start until y=2957 — because the approved section-opener
   drawing accent (.la-accent, 44px + 10px margin) sits above it
   and consumes 54px. So the two columns were box-aligned and
   TEXT-misaligned by 54px, which reads as the headline sinking.

   The accent is an approved sitewide device and is NOT removed;
   the prose simply starts on the same line the eyebrow does.
   Guarded with :has(> .la-accent) so a head WITHOUT an accent is
   untouched, and scoped above the stack breakpoint so it can
   never add dead space on a phone.
   ============================================================ */
@media (min-width:900px){
  .p-block .wrap:has(> .p-head + .prose) > .p-head:has(> .la-accent) + .prose{
    padding-top:54px}
}

/* ============================================================
   §B6 · RESOURCES CURATION — no empty tracks, no orphan rows
   ------------------------------------------------------------
   Measured at 1440 before changing anything:
     · /videos/  `.vid-grid` declared a FIXED 3 tracks regardless of
       how many clips a category holds. Three categories hold 2 and
       left 366px of empty track; the two single-clip categories sat
       in a 584px column and rendered their tile at just 177px.
     · /blog/    8 entries = 1 full-width feature + 7 cards in 3
       columns -> 3 / 3 / 1, an orphan card leaving 733px empty.

   This is the SAME defect recorded three times in this file (the
   footer, /videos/, the sector cards). The fix is the one the
   project already settled on: never auto-fill, and make the track
   count follow the real item count.
   ============================================================ */
@media (min-width:900px){
  /* videos — tracks follow the clip count, capped at three */
  .vid-grid:has(> :last-child:nth-child(1)){grid-template-columns:minmax(0,1fr)}
  .vid-grid:has(> :last-child:nth-child(2)){grid-template-columns:repeat(2,minmax(0,1fr))}
  /* ⚠ PRE-EXISTING orphan, found while verifying the above and NOT caused by
     it: the one real film carries `.is-film` and spans 2 tracks, so a category
     of film + 2 tiles needs 4 slots in a 3-track grid and dropped one tile to
     its own row beside 732px of nothing. On two tracks the film fills row one
     and the two tiles fill row two exactly. */
  .vid-grid:has(> .is-film):has(> :last-child:nth-child(3)){
    grid-template-columns:repeat(2,minmax(0,1fr))}

  /* blog — fill the last row for ANY count, the same device
     `.p-grid.c3 > :nth-last-child(2):nth-child(3n+1)` already uses.
     The lead feature spans all three tracks, so an orphan lands on
     3n+2; a two-card last row lands on 3n. */
  /* ⚠ Spanning the orphan is only half the fix. A card built for a 339px
     column, given 1072px, keeps its 4:3 media box and balloons to ~800px
     tall — the same failure mode as the blog lead's `height:100%` bug.
     So the widened card also takes the HORIZONTAL geometry `.bcard-lead`
     already defines (media beside body), which keeps its height sane and
     reads as a deliberate closing item. Column ratio is flipped against
     the lead so the two do not read as the same card twice. */
  .blog-grid > .bcard:last-child:nth-child(3n+2){
    grid-column:1/-1;display:grid;grid-template-columns:.85fr 1.15fr;
    align-items:stretch}
  .blog-grid > .bcard:last-child:nth-child(3n){
    grid-column:span 2;display:grid;grid-template-columns:.9fr 1.1fr;
    align-items:stretch}
  .blog-grid > .bcard:last-child:nth-child(3n+2) .bcard-media,
  .blog-grid > .bcard:last-child:nth-child(3n) .bcard-media{
    aspect-ratio:16/11;height:auto;min-height:0}
}


/* ============================================================
   §GS2 · GALLERY SLIDER SIZING  (2026-08-18, corrects §GS)
   ------------------------------------------------------------
   Abhay: the slides were TOO BIG — ~1.4 visible at 1440, which
   reads as one-per-view rather than a browsable set. Target is
   3-4 visible at 1440 and 1.5-2 at 390.

   ⚠ THE ARITHMETIC IS WHY THIS ISN'T JUST A WIDTH CHANGE.
   The container's inner width is 1072px at 1440. Three-up inside
   it gives (1072 - 2*gap)/3 = ~344px — which is exactly the
   ~343px thumbnail size the masonry was rejected for. So showing
   3-4 AND keeping the images large is impossible within the
   container, and the track has to be wider than the container.

   It now runs from the container's left edge to the VIEWPORT's
   right edge (negative right margin only — the left stays on the
   page text axis, so the slider still lines up with the heading
   above it). At 1440 that is 1256px of runway: slides sit at
   ~430px with three fully visible and the fourth peeking, which
   is both the requested count and 25% larger than the masonry.
   The peeking fourth is also the affordance that says "scrollable".
   ============================================================ */
/* Tuned by MEASURING visible counts, not by eye:
   1440 -> 389px slides, 3 fully visible in the 1256px runway with the 4th
   just breaking the edge · 1200 -> 324px, 3 + peek · 1920 -> 396px, 4 fully.
   Still 13% larger than the 343px masonry tile it replaced. */
.gslider{--gsw:clamp(300px,27vw,396px)}
.gs-track{margin-right:calc(-1 * max(0px,(100vw - 1072px)/2));
  padding-right:clamp(16px,3vw,48px)}
@media (max-width:1200px){
  .gs-track{margin-right:calc(-1 * max(0px,(100vw - 100%)/2))}
}
/* tablet: two per view — 66% measured only ONE fully visible at 768 */
@media (max-width:1000px){ .gslider{--gsw:46vw} }
/* phone: 1.5-2 visible, so the next frame is always half in view */
@media (max-width:560px){ .gslider{--gsw:62%} }

/* ============================================================
   §PD · SPEC READOUT + THE RELOCATED CALIPER  (Part D, 2026-08-18)
   The figure keeps the photograph's OWN 900x691 aspect box and is
   never cropped, which is what lets every coordinate in
   hero-dim.svg.php / hero-blueprint.svg.php stay valid unchanged.
   Overlays are absolutely positioned to that same box.
   ============================================================ */
.specrow-in{display:grid;grid-template-columns:1fr;gap:clamp(22px,2.6vw,40px);
  align-items:center}
@media (min-width:960px){
  /* ⚠ 2026-08-18 — was .86fr / 1.14fr, i.e. a 446px figure beside a 591px stats
     column: the photograph read as a small picture next to three huge numerals.
     The figure now leads the row at ~57% and the two halves compose. */
  .specrow-in{grid-template-columns:minmax(0,1.32fr) minmax(0,1fr)}
}
.specfig{margin:0;min-width:0}
/* ⚠ The ground is the PHOTOGRAPH'S OWN BLACK, not the light product ground.
   tilting-pad.webp is a bearing on black (sampled: corners 0,0,0), and the box
   is set to the image's exact 1.302 aspect, so the image fills it edge to edge
   with zero crop and zero inset. Pairing it with the light `--shot-ground` meant
   that whenever the photograph had not yet painted, a pale panel showed through
   and the figure read as a small image floating in a light rounded box. With a
   black ground there is nothing to see around the image at any moment. */
.specfig-shot{position:relative;border-radius:var(--r);overflow:hidden;
  aspect-ratio:900/691;background:#000}
.specfig-shot img{display:block;width:100%;height:100%;object-fit:cover}
/* both overlays share the image's box, so their viewBoxes map 1:1 */
/* ⚠ the blueprint's class is `.herobp`, NOT `.heroblue`. Written wrong first
   time: the selector matched nothing, so the SVG rendered as an unpositioned
   block inside the figure instead of an overlay. Caught by grepping the served
   page for the class rather than trusting the rule.
   Both overlays declare preserveAspectRatio="none", so each stretches to this
   box. The box is the CALIPER's 900x691 because the caliper is the approved
   signature and must map 1:1; the blueprint's 900x675 stretches 2.4% taller,
   which is imperceptible on decorative concentric linework. */
.specfig-shot .herodim,.specfig-shot .herobp{position:absolute;inset:0;
  width:100%;height:100%;z-index:2;pointer-events:none}
.specfig-shot .herobp{z-index:1}
.specfig .eframe-cap{margin-top:12px}
@media (min-width:960px){ .specrow .pv-c-spec{grid-template-columns:1fr} }
@media (min-width:960px){
  .specrow .pv-c-spec li{padding-block:14px}
}

/* ============================================================
   §E1 · STEP MODULE INSIDE A BACKDROP BAND  (Part E, 2026-08-18)
   ------------------------------------------------------------
   /cutting-tools/ "Service model": four short steps ran as ONE tall
   column with ~100px gaps, the right 60% of a 697px band empty.

   ⚠ AND a real contrast bug underneath it: every dark-scene colour
   override for `.p-step` is scoped to `.p-block.dark`, but a
   backdrop band is `.vbleed` — so the step TITLES were rendering
   in `--tx-l` (dark ink) on a black photograph. They were close to
   invisible, which is why the block read as loose body text with
   no structure. Both are fixed here.

   2x2 on desktop, single column when the band stacks. The copy
   column widens only when it actually contains steps, so every
   other backdrop band keeps its 56ch measure.
   ============================================================ */
.vbleed:has(.p-steps) .vbleed-copy{max-width:min(76%,760px)}
.vbleed .p-steps{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));
  column-gap:clamp(24px,2.6vw,44px);row-gap:0;margin-top:22px}
.vbleed .p-step{display:block;padding-block:clamp(14px,1.5vw,20px);
  border-top:1px solid rgba(255,255,255,.16)}
.vbleed .p-step:last-child{border-bottom:0}
.vbleed .p-step-n{display:block;margin-bottom:5px;color:var(--brand-lift)}
.vbleed .p-step h3{color:var(--tx-d);font-size:clamp(1rem,1.15vw,1.12rem)}
.vbleed .p-step p{color:var(--tx-d-dim);margin-top:6px;max-width:38ch;
  font-size:.9rem;line-height:1.55}
@media (max-width:860px){
  .vbleed:has(.p-steps) .vbleed-copy{max-width:none}
  .vbleed .p-steps{grid-template-columns:minmax(0,1fr)}
}

/* ============================================================
   §E2 · BANNER VARIANT OF THE FIGURE SPLIT  (Part E, 2026-08-18)
   Two short paragraphs beside each other, the figure full width
   under them. Removes the height mismatch that produced the dead
   band, and lets the heading sit tight to its own content.
   ============================================================ */
@media (min-width:861px){
  /* ⚠ `display:grid` MUST be restated. A later generic rule sets this same
     element to `display:flex/block`, which made `grid-template-columns` inert —
     the columns were declared, computed, and did nothing. Measured
     `display:block` on the served page while the tracks read correctly, which
     is the tell. */
  .p-split.p-split-banner:has(.prose > figure) .prose{
    display:grid;
    grid-template-columns:repeat(2,minmax(0,1fr));align-items:start;
    column-gap:clamp(28px,3vw,52px);row-gap:clamp(20px,2.2vw,30px)}
  .p-split.p-split-banner:has(.prose > figure) .prose > p{max-width:46ch}
  .p-split.p-split-banner:has(.prose > figure) .prose > figure{grid-column:1/-1}
  /* 16/6 made the section TALLER than the defect it replaced (624 -> 785).
     16/4.6 keeps the banner emphatic while bringing the section back under the
     original height, with the dead band still gone. */
  .p-split.p-split-banner:has(.prose > figure) .prose > figure .eframe{
    aspect-ratio:16/4.6}
  /* the heading no longer needs the full-width lead-in rule: its content
     starts immediately beneath it */
  .p-split.p-split-banner:has(.prose > figure) > :first-child{
    padding-bottom:clamp(12px,1.4vw,18px)}
}
