/* =========================================================
   EDC - Engineered Devices Corporation
   Industrial design system.
   Tokens (sampled from the official EDC seal):
   ink #221e1f (structure, seal's outer lettering), charcoal
   #414042 (seal's inner ring, secondary text), brand blue
   #007cbb (THE blue: accents, borders, BUILD word, active
   underlines), blue-button #006ca3 (primary button fill,
   5.72:1 with white text), navy #0a3d62 / navy-deep #12304a
   (derived deep fields: footer, plates, map core, inversions;
   never headings). Barlow Condensed headings, Barlow body.
   No border-radius anywhere. Hard edges. Full-bleed sections.
   Fonts load via <link> in each page head (not @import).
   ========================================================= */

:root{
  --ink:#221e1f;          /* near-black (seal outer lettering) - headings, structure */
  --steel:#414042;        /* charcoal (seal inner ring) - secondary text */
  --charcoal:#414042;     /* alias */
  --muted:#5f6a73;
  --line:#d9dee3;
  --paper:#ffffff;
  --concrete:#f2f3f4;     /* alternate section background */
  --accent:#f2f3f4;       /* legacy alias kept for inline styles */
  --navy:#0a3d62;         /* derived from brand blue - deep fields only */
  --navy-deep:#12304a;    /* deepest field */
  --blue:#007cbb;          /* THE brand blue, straight off the seal - accents/borders/BUILD */
  --blue-button:#006ca3;   /* primary button fill - 5.72:1 w/ white text */
  --blue-button-hover:#005c8a;
  --blue-tint:#e3f0f7;     /* light tint - hover/backgrounds only */
  --container:1200px;
  /* Subpage banner frame height. Every .page-banner is this tall, on every
     subpage, so the site's header band never jumps between pages. Trimmed ~10%
     on 2026-08-03 (owner: "slightly shorter") from 466/428/378. The floor is
     the tallest banner CONTENT in each band - always STENdeck, which alone
     carries a logo + title + subtitle + button: 344px desktop (its title wraps
     at ~1150), 325px tablet (at 960), 309px phone (at 320). Frame minus that
     is what .page-banner has left for padding; see the padding note there.
     Overridden per breakpoint below. */
  --banner-h:420px;
  --head:'Barlow Condensed',sans-serif;
  --body:'Barlow',sans-serif;
}

*{box-sizing:border-box;margin:0;padding:0}
html{scroll-behavior:smooth}
body{
  font-family:var(--body);
  color:var(--ink);
  font-size:17px;
  line-height:1.6;
  background:var(--paper);
  -webkit-font-smoothing:antialiased;
}
h1,h2,h3,h4{font-family:var(--head);font-weight:600;line-height:1.05;color:var(--ink);
  text-transform:uppercase;letter-spacing:.02em}
h1{font-size:clamp(2.6rem,6vw,4.2rem)}
h2{font-size:clamp(2rem,3.8vw,2.9rem)}
h3{font-size:1.65rem}
/* trademark-case guard: STENdeck must never render STENDECK */
.tm{text-transform:none}
a{color:inherit;text-decoration:none}
img{max-width:100%;height:auto;display:block}
p{color:var(--steel)}

.container{max-width:var(--container);margin:0 auto;padding:0 24px}
.section{padding:110px 0}
.section--tight{padding:64px 0}
.center{text-align:center}
.eyebrow,.kicker{
  font-family:var(--head);font-weight:700;letter-spacing:.18em;
  text-transform:uppercase;color:var(--steel);font-size:1.05rem;margin-bottom:14px;
}
/* --blue-button, not --blue: the eyebrow runs at 16.8px, which is UNDER the
   18.66px bold large-text threshold, so it needs the full 4.5:1. --blue #007cbb
   measures 4.11:1 on --concrete (fails); --blue-button #006CA3 measures 5.15:1
   on --concrete and 5.72:1 on white. Same reason button fills already use it. */
.eyebrow b,.kicker b{color:var(--blue-button);font-weight:700}
.lead{font-size:1.08rem;max-width:720px}
.center .lead{margin-left:auto;margin-right:auto}

/* ---------- Buttons - ONE primary style, square ---------- */
.btn{
  display:inline-flex;align-items:center;gap:.5rem;
  font-family:var(--head);font-weight:600;font-size:1.02rem;line-height:1.1;
  text-transform:uppercase;letter-spacing:.08em;
  padding:15px 32px;border:2px solid transparent;cursor:pointer;
  white-space:nowrap;
  transition:background .15s,color .15s;
}
.btn--primary{background:var(--blue-button);color:#fff}
.btn--primary:hover{background:var(--blue-button-hover)}
.btn--ghost{background:transparent;color:var(--ink);border-color:var(--ink)}
.btn--ghost:hover{background:var(--ink);color:#fff}
.btn--white{background:#fff;color:var(--ink)}
.btn--white:hover{background:var(--concrete)}
/* ghost buttons sitting on dark imagery/bands invert */
.hero .btn--ghost,.band .btn--ghost,.cta .btn--ghost,.page-banner .btn--ghost{
  color:#fff;border-color:#fff}
.hero .btn--ghost:hover,.band .btn--ghost:hover,.cta .btn--ghost:hover,
.page-banner .btn--ghost:hover{background:#fff;color:var(--ink)}

/* ---------- Utility bar + Header (white, wordmark left) ---------- */
/* DEVICE-PIXEL SNAP, part 1 of 2 (2026-08-10). Height was content-driven and
   measured 38.3125px, which put everything below it - including the brand seal
   - on a fractional row and cost 8.7% of the seal's edge contrast (measured via
   canvas raster at the real offsets). 39px is ceil() of the content height, so
   nothing clips, and it also pins the bar against a font-load reflow. The
   mobile block below overrides this with --util-h:40px. */
.util-bar{background:var(--navy);color:#cfd6db;font-size:.85rem;height:39px}
.util-in{display:flex;justify-content:flex-end;align-items:center;gap:28px;padding:7px 24px;
  max-width:var(--container);margin:0 auto}
.util-bar a:hover{color:#fff}
.util-tel{font-family:var(--head);font-weight:600;letter-spacing:.08em;color:#fff;font-size:.95rem}

.site-header{
  position:sticky;top:0;z-index:50;background:var(--paper);
  border-bottom:1px solid var(--line);
}
.nav{display:flex;align-items:center;justify-content:space-between;gap:16px;height:108px}
/* DEVICE-PIXEL SNAP, part 2 of 2 (2026-08-10). .container centres with auto
   margins, so on an odd layout width the offset lands on .5 - at a 1440
   viewport (1425 after the scrollbar) the brand seal started at x=136.5 and the
   whole mark was composited across the pixel grid, costing 10.2% of its edge
   contrast. Flooring the centring offset to whole pixels fixes it. SCOPED TO
   THE HEADER: page containers keep plain auto margins, because several layouts
   below are measured against exact centring. round() is Chromium 130+/FF 118+/
   Safari 15.4+; where it is unsupported the whole declaration is invalid and
   the element falls back to the inherited margin:0 auto, i.e. today's
   behaviour. Verified: seal left 136.5 -> 136, top 48.3125 -> 48 at 1440. */
.site-header .nav{
  margin-left:max(0px, round(down, calc((100% - var(--container))/2), 1px));
  margin-right:auto;
}
/* Brand lockup: cropped seal + wordmark inside one link. .brand-seal is a
   square crop of edc-seal.png taken at the outer edge of the black sunburst,
   with the text ring cropped away, shipped as the edc-seal-mark-*.webp srcset
   ladder (176/256/384/640w - the 96w and 128w rungs were dropped so the
   browser can never pick a source below 2x the render size). Both marks share
   the single height rule below, so the 680/560 step-downs carry the seal along
   with the wordmark and neither can be crushed independently - .brand is also
   flex-shrink:0 because brand crushing was a fixed bug, do not drop it. */
.brand{display:flex;align-items:center;gap:12px;flex:0 0 auto}
.brand img{height:40px;width:auto;flex:0 0 auto}
/* Nav fit (2026-07-23): inter-link gap 26->16 + .nav gap 24->16 +
   .nav-cta .btn 32->24 (below) reclaim ~102px of the desktop nav's width so
   the hamburger breakpoint can sit lower. Spacing only - the link SET is
   unchanged. At mobile the panel overrides gap to 0, so this is desktop-only. */
.nav-links{display:flex;align-items:center;gap:16px;list-style:none}
.nav-links>li{position:relative}
.nav-links a{
  font-family:var(--head);font-weight:600;font-size:1.02rem;letter-spacing:.05em;
  text-transform:uppercase;color:var(--ink);padding:8px 0;display:inline-block;
}
.nav-links>li>a[aria-current="page"]{box-shadow:inset 0 -3px 0 var(--blue)}

/* deep dropdowns + nested flyout */
.has-drop{position:relative}
.dropdown,.flyout{
  position:absolute;top:100%;left:0;background:var(--paper);
  border:1px solid var(--line);border-top:3px solid var(--ink);
  min-width:280px;padding:6px 0;list-style:none;z-index:60;
  opacity:0;visibility:hidden;transition:opacity .12s;
}
/* hover states live behind a pointer gate: on touch, tap-synthesized hover
   sticks after the finger lifts (the mobile accordion owns reveal there) */
@media (hover:hover) and (pointer:fine){
  .nav-links a:hover{color:var(--blue)}
  .has-drop:hover>.dropdown,.has-fly:hover>.flyout{opacity:1;visibility:visible}
  .dropdown a:hover,.flyout a:hover{background:var(--blue-tint);color:var(--ink)}
}
/* keyboard reveal: desktop widths only - at mobile widths :focus-within on a
   toggle button would force collapsed submenus visible/focusable.
   NAV BREAKPOINT = 1140 (2026-08-10; was 1080 from 2026-07-23, 961 before).
   The 8-link nav needed ~1049px of layout width on its own; adding the brand
   seal to the lockup pushed the requirement to ~1102px, which does not fit a
   1080 viewport once the 15px scrollbar is taken out (1017px of content
   against 1053.67px needed = 36.67px over). 1140 gives 1077px of content
   against 1061px needed at the clamp's 40px floor: 16.33px of slack, which is
   the safety margin the seal clamp is solved for.
   This is INDEPENDENT of the page-layout grids (which still switch at 960)
   and of the pnav breakpoint (960). DO NOT UNIFY THEM. See FIXED TOP UNIT. */
@media (min-width:1140px){
  .has-drop:focus-within>.dropdown,.has-fly:focus-within>.flyout{opacity:1;visibility:visible}
  /* Fluid brand seal. The seal takes whatever width is left after the fixed
     parts of the nav; the WORDMARK NEVER SCALES (see .brand img, 40px).
     Derivation - the fixed cost of the desktop nav is 1001.67px measured:
       wordmark 224.45 + links 571.28 + CTA 173.94 + 2 x 16px .nav gaps.
     Available content = min(100vw - 15 scrollbar, 1200 --container) - 48 padding.
     Budget for the seal = available - 1001.67 - 12 lockup gap - 16 safety.
     Below the container cap that is (100vw - 1093px), which is the middle term.
     100vw INCLUDES the scrollbar, which is why the constant carries the 15.
     MAX 88px, not more: --container caps at 1200, so available width plateaus
     at 1152 from ~1215px viewport up - the nav gains nothing beyond that, and
     88px leaves 10px of air top and bottom in the 108px nav row.
     MIN 40px is the floor below which the seal reads as a badge stuck on the
     side of the wordmark rather than half of a lockup. The floor needs ~1117px
     of viewport, which is why the nav breakpoint moved to 1140 (above). */
  /* Height is rounded to an EVEN number of pixels. The nav row is 108px and
     centres with align-items, so an odd seal height puts the mark on a .5 row
     (47px gave top:69.5) and re-introduces the sub-pixel blur the two snap
     rules above exist to remove. Costs at most 1px of seal. */
  .brand .brand-seal{height:clamp(40px, round(down, 100vw - 1093px, 2px), 88px)}
}
.has-fly{position:relative}
.flyout{top:-7px;left:100%}
.dropdown a,.flyout a{display:block;padding:11px 18px;font-size:.92rem;text-transform:none;
  letter-spacing:.02em;font-family:var(--body);font-weight:500}
/* mobile accordion parts (see build_nav.py): desktop never shows them */
.drop-toggle{display:none}
.m-only{display:none}
.nav-cta{display:flex;align-items:center;gap:16px}
.nav-cta .btn{padding:15px 24px}
.menu-toggle{display:none;background:none;border:0;cursor:pointer;font-size:1.6rem;color:var(--ink)}

/* ---------- Hero - photography-led, full-bleed ---------- */
.hero{position:relative;background:var(--ink);color:#fff;overflow:hidden}
.hero::after{content:"";position:absolute;inset:0;
  background:linear-gradient(100deg,rgba(10,13,15,.88) 0%,rgba(10,13,15,.55) 55%,rgba(10,13,15,.25) 100%)}
.hero-bg{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;object-position:center}
.hero-inner{position:relative;z-index:2;padding:150px 0 160px;max-width:780px}
.hero h1{color:#fff}
.hero p{color:#d5dbe0;font-size:1.15rem;margin:24px 0 36px;max-width:560px}
.hero .eyebrow,.hero .kicker{color:#aeb9c2}
.hero-actions{display:flex;gap:14px;flex-wrap:wrap}

/* ---------- Hero slideshow (homepage only) ----------
   Slides are stacked absolutely and cross-faded by opacity; the .hero-inner
   title/buttons sit above them at z-index:2 and NEVER move or re-animate -
   only the photo behind them changes. Arrows/dots sit at z-index:3 so they
   clear the .hero::after gradient. Slide list lives in js/hero.js. */
.hero-slides{position:absolute;inset:0}
.hero-slides .hero-bg{opacity:0;transition:opacity .9s ease-in-out}
.hero-slides .hero-bg.is-active{opacity:1}
/* Glyph only - no plate, no border, no default button chrome. The drop-shadow
   keeps the white arrow readable where a slide is light behind it. */
.hero-arrow{position:absolute;top:50%;transform:translateY(-50%);z-index:3;
  width:56px;height:72px;display:grid;place-items:center;padding:0;
  background:none;border:0;color:#fff;cursor:pointer;opacity:.8;
  -webkit-appearance:none;appearance:none;
  transition:opacity .2s ease,color .2s ease}
.hero-arrow svg{width:26px;height:26px;fill:currentColor;display:block;
  filter:drop-shadow(0 1px 3px rgba(0,0,0,.65))}
.hero-arrow:hover{opacity:1;color:var(--blue)}
/* Kill the default focus box, then put a real indicator back: the glyph goes
   blue and a hard-edged blue bar appears under it. Written :focus-first so a
   browser without :focus-visible still shows an indicator (rather than being
   left with outline:none and nothing) - the :not(:focus-visible) pair then
   hides it again for mouse clicks where support exists. */
.hero-arrow:focus{outline:none;opacity:1;color:var(--blue)}
.hero-arrow:focus::after{content:"";position:absolute;
  left:14px;right:14px;bottom:14px;height:3px;background:var(--blue)}
.hero-arrow:focus:not(:focus-visible){opacity:.8;color:#fff}
.hero-arrow:focus:not(:focus-visible)::after{content:none}
.hero-arrow--prev{left:0}
.hero-arrow--next{right:0}
.hero-dots{position:absolute;left:0;right:0;bottom:14px;z-index:3;
  display:flex;justify-content:center;gap:6px}
/* Deliberately minimal: the VISIBLE mark is a 16x2 hairline, but the button
   keeps a 44px-tall (transparent) hit area so shrinking the look does not
   shrink the tap target. Padding does the hiding, not a smaller button. */
.hero-dots button{width:32px;height:44px;padding:21px 8px;box-sizing:border-box;
  background:none;border:0;cursor:pointer}
.hero-dots button i{display:block;height:2px;background:rgba(255,255,255,.35)}
.hero-dots button[aria-current="true"] i{background:var(--blue)}
.hero-dots button:focus-visible{outline:3px solid var(--blue);outline-offset:-3px}
@media(prefers-reduced-motion:reduce){
  .hero-slides .hero-bg{transition:none}
}

/* ---------- Page banner (subpages) - ink, optional photo ---------- */
/* One fixed frame height for every subpage banner (--banner-h). Height used to
   be purely content-driven, so a page with a long title, a subtitle and a
   button (STENdeck) sat 160px taller than one with a title alone (Partners).
   min-height sets the floor and the content is centred in it, so short banners
   pad out instead of collapsing.
   The padding is NOT the visible spacing any more - centring is. It is the
   emergency gutter for a banner whose content is taller than the frame, and
   because it adds to that content it is also what decides how short the frame
   can be (content + 2*padding must stay under --banner-h or that page grows
   and stops matching). It was 96/64/48 when it did the spacing; trimmed to
   32/28/16 in the 2026-08-03 height cut so the shorter frame still clears
   STENdeck's tallest wrap in every band with ~12px to spare. Raise --banner-h
   before raising this back.
   NOT applied to the homepage .hero - different element, deliberately taller. */
.page-banner{position:relative;background:var(--ink);color:#fff;padding:32px 0;overflow:hidden;
  min-height:var(--banner-h);display:flex;flex-direction:column;justify-content:center}
.page-banner .bg{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:.34}
/* width:100% is load-bearing, do not drop it. .container is margin:0 auto with
   no width of its own; that is fine in normal flow, but .page-banner is a flex
   column, and auto side margins on a flex item cancel the default stretch and
   shrink it to its widest child. The banner text then centred itself on every
   page, at a different offset per page depending on how long the title was
   (Contact Us started 424px in at 1280, STENdeck 73px in). width:100% restores
   the 1200px container gutter so every banner's text starts on the same line. */
.page-banner .container{position:relative;z-index:2;width:100%}
.page-banner h1{color:#fff}
.page-banner p{color:#c6cfd6;max-width:640px;margin:14px 0 0}
.page-banner .eyebrow{color:#aeb9c2}
/* About Us crew-photo banner: replicate the landing photo-band exactly - full
   brightness so the faces read, navy plate pinned BOTTOM-LEFT so it never
   covers the people. SCOPED to --crew; every other page banner keeps the
   .34 dim + overlaid title. Plate is a direct child of the section here (no
   .container), so left:0 pins it flush to the section edge like the band.
   Frame height comes from --banner-h like every other banner (it was a
   one-off 520px until 2026-08-03); padding:0 so the plate sits flush. */
.page-banner--crew{padding:0}
/* Y anchor 34%, NOT center. The slot is a fixed --banner-h but the image scales
   with viewport width, so the wider the screen the less source height survives
   the cover crop: 964px of source at 768 wide, but only 315px at 2560.
   Centering pushed that shrinking window down past the crew - at 2560 it started
   at source y=562, below the hard hats, leaving vests and torsos. Measured face
   band is source y=389 (highest hat crown) to y=680 (lowest chin) of 1440.
   Holding BOTH edges across 320-2560 allows p = 0.324-0.346; 2560 is what binds
   it. Within that band, raising p slides the window down: heads sit higher in
   the frame and more torso shows, which is what the owner asked for (2026-07-31,
   "up a bit more so you can still see the body"), so sit near the top of it.
   34% leaves 7px of air above the tallest hat and 18px of body below the lowest
   chin at 2560 (42px / 87px at 1920). 0.3458 is the hard ceiling - past it that
   hat clips at 2560. Do NOT nudge past 34.6% without re-checking 2560; 40%
   looks better at 1280-1920 but decapitates at ultrawide. Past ~2771px wide the
   window is shorter than the 291px face band and NO value works - re-cut the
   asset if the design goes wider. Scoped to --crew: the other 36 pages keep the
   centred overlay. */
.page-banner--crew .bg{opacity:1;object-position:center 34%}
/* Plate width is capped at 20% of the banner, NOT 600px, and that number is
   measured, not chosen: hard-hat detection on the source puts man 1 (green
   vest, far left) at x 21.7%-30.9% of the image width, and from 561px up the
   cover crop shows the full image width, so 21.7% of the banner is exactly
   where his hat brim starts. 20% stops short of it with 17px of clearance at
   961 and 33px at 1920 - and the brim is wider than the face, so the real
   margin is larger. The old 600px plate ran to 47% at 1280 and buried him.
   Side padding drops 52px -> 28px with it: 52px of gutter inside a 192px plate
   left ~90px for text, which pushed the subtitle to 9 lines and made the plate
   530px tall inside a 420px banner - its top was being cut off by the section's
   overflow:hidden. At 28px every desktop width fits with 27-174px to spare.
   The title now wraps to two lines below 1600px ("ABOUT / US"). That is
   accepted (owner, 2026-08-03) - it is the price of clearing the man's face.
   THIS ONLY WORKS ON DESKTOP. The 681-960 and <=680 blocks put the plate back
   to 600px, because 20% there is 154px and 75px - far too narrow for the text.
   See those blocks; the numbers are in HANDOFF.md. */
.page-banner--crew .plate{position:absolute;left:0;bottom:0;z-index:2;background:var(--navy);color:#fff;padding:32px 28px;max-width:20%}
/* #8b969e measured 3.75:1 on the --navy plate (AA needs 4.5). #a0adb6 = 4.93:1
   - the minimum lift that clears AA while staying dimmer than the plate's
   #fff body text, so the breadcrumb hierarchy is unchanged. */
.crumbs{font-family:var(--head);font-size:.9rem;letter-spacing:.08em;text-transform:uppercase;
  color:#a0adb6;margin-bottom:12px}
.crumbs a:hover{color:#fff}

/* ---------- Photo band - full-bleed jobsite photo + plate ---------- */
.photo-band{position:relative;min-height:520px;background-size:cover;background-position:center}
/* Plate is --concrete, not --navy (2026-08-03). It carries the DARK EDC seal
   now, and the seal's near-black outer ring disappears on navy - that is the
   same constraint that forces edc-seal-reversed.png on the footer and other
   deep fields. Light plate, dark seal, dark text: --ink heading measures
   14.84:1 on --concrete and --steel body text 9.28:1, both far past AA's 4.5.
   Used on index.html only - no other page has a .photo-band. */
.photo-band .plate{position:absolute;left:0;bottom:0;background:var(--concrete);color:var(--ink);
  padding:42px 52px;max-width:600px}
/* Headline runs SMALLER than the global h2 clamp here (owner, 2026-08-04): at
   the global clamp's 46.4px the copy broke to SEVEN lines and the plate grew to
   503px inside a 520px band, so it ran the full height of the panel. This clamp
   is the same curve scaled ~27% down (32/3.8vw/46.4 -> 24/2.7vw/34), so every
   width shrinks proportionally rather than only the desktop end.
   34px is the max on purpose: it sets FOUR lines ending on "Northeastern U.S.
   and Florida.", which lands the block where the "STENdeck X Slab Shoring" line
   used to sit. 36px also lands there but breaks to five lines and leaves
   "Florida." orphaned alone on the last one - do not raise it back without
   re-checking the rag. Copy is unchanged; this is type size only. */
.photo-band .plate h2{color:var(--ink);font-size:clamp(1.5rem,2.7vw,2.125rem)}
.photo-band .plate p{color:var(--steel);margin-top:12px}
/* Seal + STENdeck X lockup. Heights are set, widths are auto, so both scale
   proportionally and neither can distort. Both are downscales - the seal is a
   1098px PNG shown at 52, the wordmark is vector - so nothing upscales at any
   DPR. The two heights differ on purpose: the seal is square and the wordmark
   is 7.7:1, so matching their HEIGHTS would leave the wordmark visually twice
   the mass. 52 against 26 is what reads as equal weight, and the 2:1 ratio is
   what must be preserved if these are ever resized again.
   Scaled up 2026-08-04 (owner) from 52/26 to 68/34 - the same 2:1, x1.31 - so
   they read against the panel. ONLY the height is set and width stays auto, so
   both scale proportionally: the wordmark stays at its 7.73:1 and is never
   stretched. Neither upscales: the seal is a 1098px PNG at 68 (retina wants
   136) and the wordmark is vector.
   The bigger pair is held to DESKTOP. Below 680 the plate narrows to the
   viewport (312px of usable width at 375) and 68/34 needs 357px, so it would
   overflow; the <=680 block puts back the smaller pair. align-items:center is
   unchanged, so the two marks keep the same left-aligned start as before.

   THE 2:1 RULE ABOVE IS SUPERSEDED (owner, 2026-08-06). The seal alone was
   scaled to 96 desktop / 68 mobile; the wordmark stayed at 34 / 26. That is
   2.82:1 and 2.62:1, so the seal now deliberately outweighs the wordmark - the
   instruction was "make the EDC seal larger", not "scale the lockup". Do not
   "restore" the ratio by growing the wordmark to 48; that is a different change
   and was not asked for. Still no upscaling: the seal is a 1098px PNG at 96,
   and retina wants 192. Widths stay auto, so neither mark is ever stretched.
   Horizontal fit re-checked, both pairs clear their row: desktop 96+26+262.8 =
   384.8 in a 496px content box, mobile 68+26+200.9 = 294.9 in 327px. */
.photo-band .plate-marks{display:flex;align-items:center;gap:26px;margin-top:26px}
.photo-band .plate-marks img{display:block;width:auto}
.photo-band .plate-marks .mark-seal{height:96px}
.photo-band .plate-marks .mark-word{height:34px}
/* "Learn More" -> the STENdeck press release. Mirrors .card .more's look
   (uppercase condensed 600, --ink, 2px --ink underline) rather than reusing it,
   because that rule is scoped to .card and its margin-top:auto/padding-top:16px
   belong to a flex card, not to this plate. The base .more supplies the 24px
   min hit area (2.5.8); 18px margin-top puts it on its own row under the
   headline. Hover is --blue-button, NOT --blue: #007CBB measures 4.10:1 on
   --concrete and fails AA at this 16px/600 size, #006CA3 measures 5.14:1. Same
   substitution and same reason as the .eyebrow b rule near the top. */
.photo-band .plate .more{margin-top:18px;color:var(--ink);font-family:var(--head);
  font-weight:600;text-transform:uppercase;letter-spacing:.06em;
  border-bottom:2px solid var(--ink);padding-bottom:2px}
.photo-band .plate .more:hover{color:var(--blue-button);border-color:var(--blue-button)}

/* ---------- Stats - dark band ---------- */
.stats{background:var(--ink);color:#fff}
.stats .grid{display:grid;grid-template-columns:repeat(4,1fr);gap:20px;text-align:center}
.stat{border-left:1px solid rgba(255,255,255,.15)}
.stat:first-child{border-left:0}
.stat .num{font-family:var(--head);font-size:3.4rem;font-weight:700;line-height:1;color:#fff}
.stat .label{color:#9aa5ad;font-size:.9rem;letter-spacing:.1em;text-transform:uppercase;
  font-family:var(--head);margin-top:8px}

/* ---------- Cards / services ---------- */
.grid{display:grid;gap:26px}
.cols-3{grid-template-columns:repeat(3,1fr)}
.cols-2{grid-template-columns:repeat(2,1fr)}
.card{
  background:var(--paper);border:1px solid var(--line);
  padding:34px;height:100%;
  display:flex;flex-direction:column;align-items:flex-start;
}
.card>*{width:100%}
.card .icon{
  width:60px;height:60px;background:var(--concrete);color:var(--ink);
  display:grid;place-items:center;margin-bottom:20px;font-size:1.5rem;
}
.card .icon svg{width:30px;height:30px}
.card .thumb{width:auto;align-self:stretch;margin:-34px -34px 24px;height:220px;overflow:hidden}
.card .thumb img{width:100%;height:100%;object-fit:cover}
.card h3{margin-bottom:10px}
/* WCAG 2.5.8 (24px min target). The padding-top below is scoped to .card, so a
   standalone .more CTA outside a card ("Read the press release", "Download
   brochure") had no padding at all and measured 20px tall. This grows the HIT
   AREA only - no change to type size, colour, or weight - and is inert inside a
   .card, where padding-top:16px already clears 24px.
   NOT applied to inline links inside sentences (rebar "catalogs page", the
   catalogs tel link): 2.5.8 exempts targets whose size is constrained by the
   line-height of surrounding text, and forcing them taller would break
   paragraph flow. */
.more{display:inline-flex;align-items:center;min-height:24px}
.card .more{display:inline-block;margin-top:auto;padding-top:16px;width:auto;
  color:var(--ink);font-family:var(--head);
  font-weight:600;text-transform:uppercase;letter-spacing:.06em;border-bottom:2px solid var(--ink)}
.card .more:hover{color:var(--blue);border-color:var(--blue)}

/* ---------- Two-column feature ---------- */
.split{display:grid;grid-template-columns:1fr 1fr;gap:64px;align-items:center}
.split img{width:100%;object-fit:cover;object-position:center}
.split.reverse .split-text{order:2}
/* Two photos side by side in one .split column (formwork.html "Standard &
   Custom"). The column fractions ARE the two photos' aspect ratios, so each
   image's width/AR - i.e. its height - comes out identical, and both sit at
   their true proportions: no cover-crop, no squash. .split img above sets
   object-fit:cover, which is inert here because height stays auto and the box
   therefore matches the intrinsic ratio exactly.
   Stacks below 480: at 375 the two columns would be 112px and 199px wide. */
.split-pair{display:grid;grid-template-columns:.75fr 1.3333fr;gap:16px;align-items:start}
/* The .split-pair--eq equal-column variant was removed 2026-08-04 when the
   stendeck.html intro reverted to a single photo; that section was its only
   consumer. .split-pair itself is kept deliberately (owner) even though it now
   has no consumer either, since the paired layout is likely to return. */
@media(max-width:479px){
  .split-pair{grid-template-columns:1fr;gap:20px}
}
/* Two short blurbs side by side INSIDE a .split text column (rebar.html
   splicing section: Standard Coupler / Positional Coupler). They used to stack,
   which cost ~130px of height in a column already shorter than the graphic
   beside it. Stacks at <=680, where .split itself is already one column and two
   sub-columns would be ~150px wide. The h3 override is needed because the
   global 1.65rem wraps "Positional Coupler" onto two lines in a ~260px cell. */
.coupler-pair{display:grid;grid-template-columns:1fr 1fr;gap:24px;margin-top:24px}
.coupler-pair h3{font-size:1.2rem;margin-bottom:6px}
.eq-list{list-style:none;margin-top:20px}
.eq-list li{display:flex;gap:12px;align-items:flex-start;margin-bottom:12px;color:var(--ink)}
.eq-list li::before{content:"▪";color:var(--ink);font-weight:700}

/* ---------- Projects ---------- */
.project-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:26px}
.project{position:relative;overflow:hidden;aspect-ratio:4/5;display:block;background:var(--ink)}
.project img{width:100%;height:100%;object-fit:cover;transition:opacity .2s}
.project:hover img{opacity:.75}
.project .overlay{
  position:absolute;inset:0;display:flex;flex-direction:column;justify-content:flex-end;
  padding:26px;color:#fff;background:linear-gradient(to top,rgba(10,13,15,.92),rgba(10,13,15,0) 60%);
}
.project .tag{font-family:var(--head);font-size:.9rem;letter-spacing:.12em;
  text-transform:uppercase;color:#aeb9c2}
.project h3{color:#fff;margin-top:6px;font-size:1.5rem}
.project p{color:#c6cfd6;font-size:.95rem;margin-top:8px}
/* filter safety: hidden must beat the grid display rules */
.project[hidden]{display:none!important}

/* ---------- Filters: two stacked segmented controls ---------- */
.filter-stack{display:grid;gap:14px;justify-content:center;margin-bottom:20px}
.filter-row{display:flex;align-items:center;gap:18px}
.filter-label{font-family:var(--head);font-weight:700;text-transform:uppercase;
  letter-spacing:.14em;font-size:.95rem;color:var(--steel);min-width:80px;text-align:right}
.filter-bar{display:flex;border:2px solid var(--ink);width:max-content}
.fbtn{font-family:var(--head);font-weight:600;font-size:.95rem;text-transform:uppercase;
  letter-spacing:.06em;background:none;border:0;border-right:2px solid var(--ink);
  padding:12px 22px;cursor:pointer;color:var(--ink);min-height:44px}
.fbtn:last-child{border-right:0}
.fbtn.is-active{background:var(--navy);color:#fff}
.fbtn:disabled{opacity:.35;cursor:not-allowed}
.filter-count{text-align:center;font-family:var(--head);font-weight:600;
  text-transform:uppercase;letter-spacing:.1em;font-size:.85rem;color:var(--muted);
  margin-bottom:44px}
.filter-empty{text-align:center;padding:48px 0}
.filter-empty p{margin-bottom:22px;font-size:1.08rem}
.project-grid[hidden]{display:none!important}
@media(max-width:960px){
  .filter-row{flex-direction:column;align-items:stretch;gap:8px}
  .filter-label{text-align:left;min-width:0}
  .filter-bar{flex-wrap:wrap;width:auto}
  .fbtn{flex:1 1 auto;border-top:1px solid var(--line)}
}

/* ---------- Service-area map ---------- */
.area-map{width:100%;max-width:480px;justify-self:center}
.area-map rect{stroke:var(--paper);stroke-width:4}
.area-map .core{fill:var(--navy)}
.area-map .dealer{fill:var(--blue)}
.area-map .ctx{fill:#e4e7ea}
.area-map text{fill:#fff;font-weight:600}
.area-map text.t-ctx{fill:var(--muted)}
.area-map text.gap{fill:var(--muted);font-size:30px;letter-spacing:6px}
.map-legend{list-style:none;margin:24px 0 32px;display:grid;gap:10px}
.map-legend li{color:var(--ink)}
.map-legend .sw{display:inline-block;width:18px;height:18px;vertical-align:-3px;margin-right:10px}
.sw--core{background:var(--navy)}
.sw--dealer{background:var(--blue)}

/* ---------- Tables ---------- */
.spec{overflow-x:auto;margin-top:26px;border:1px solid var(--line)}
table{width:100%;border-collapse:collapse;min-width:520px}
th,td{padding:13px 18px;text-align:left;font-size:.95rem;border-bottom:1px solid var(--line)}
thead th{background:var(--ink);color:#fff;font-family:var(--head);font-weight:600;
  text-transform:uppercase;letter-spacing:.05em}
tbody tr:nth-child(even){background:var(--concrete)}
tbody td{color:var(--ink)}

/* ---------- Partners ---------- */
.partners{background:var(--concrete)}
.partners.section--tight{padding:52px 0}
/* full-color logos per owner rule (same as the marquee); grayscale removed 2026-07-16 */
.partner-grid{display:grid;grid-template-columns:repeat(5,1fr);gap:34px;align-items:center}
.partner-grid img{max-height:52px;width:auto;margin:0 auto}

/* ---------- Partner marquee: pure CSS conveyor belt ----------
   Track holds EXACTLY THREE copies of the 12-logo set (duplicated in the
   HTML). calc(-100%/3) therefore equals exactly one set width, so the
   reset lands on an identical frame and is invisible by construction.
   WHY THREE, NOT TWO (fixed 2026-07-21): at the loop end the track has
   scrolled one set, so only (trackWidth - oneSet)px of content is still
   covering the bar. With two copies that was exactly one set, so any
   viewport WIDER than a set ran past the track's right edge and logos
   visibly popped in from the right (measured at the old 9-logo / 2160px
   size: 25px of empty bar at 2200px, 385px at 2560px; seamless at
   <=1920px, which is why it hid on normal monitors). Three copies leave
   2x the set width of cover - 5760px at 12 logos, up from 4320px at 9.
   If the bar ever needs more, add a FOURTH copy and change this to
   calc(-100%/4) - keep every copy identical.
   !!! THE DURATION IS NOT A FREE PARAMETER - IT ENCODES A SPEED. !!!
   The keyframe distance is one SET, so the seconds must track the set
   width or the belt silently changes pace. A cell is 180 + 2x30 margin =
   240px, so a set is 240 x logoCount:
        9 logos = 2160px / 72s = 30px/s   (2026-07-21 to 2026-08-06)
       12 logos = 2880px / 96s = 30px/s   (2026-08-06: +SureBuilt,
                                           +Greif, +Owens Corning)
   Leaving it at 72s when those three went in would have run the belt at
   40px/s, a third faster, with nothing in the diff to show why.
   RECOMPUTE seconds = 240 x logoCount / 30 WHENEVER A LOGO IS ADDED OR
   REMOVED. Measured after this change: 30.0px/s.
   Deliberately NOT gated on prefers-reduced-motion: the belt always
   runs (owner's call, 2026-07-15). Hover or keyboard focus pauses it. */
.pm{overflow:hidden;background:var(--concrete);padding:12px 0}
.pm-track{display:flex;align-items:center;width:max-content;
  animation:pm-scroll 96s linear infinite}
.pm:hover .pm-track,
.pm:focus-within .pm-track{animation-play-state:paused}
@keyframes pm-scroll{from{transform:translate3d(0,0,0)}to{transform:translate3d(calc(-100%/3),0,0)}}
.pm-cell{width:180px;height:64px;flex:none;display:flex;align-items:center;justify-content:center;
  margin:0 30px}
.pm-cell img{max-width:100%;max-height:100%;width:auto;height:auto;object-fit:contain;
  -webkit-user-drag:none}
.pm-cell.tall img{max-height:56px}
.pm-cell:focus-visible{outline:2px solid var(--blue);outline-offset:6px}

/* ---------- Dark band (STENdeck / feature) ---------- */
.band{background:var(--ink);color:#fff;padding:64px}
.band h2{color:#fff}
.band p{color:#c6cfd6}
.band .eyebrow{color:#aeb9c2}
.band.split{align-items:center}

/* ---------- Mission / vision ----------
   NO CONSUMER as of 2026-08-06: index.html was the only page using .mv, and the
   owner deleted the Our Mission / Our Vision card pair along with the --concrete
   section that held them. Kept deliberately, same call as .split-pair and .step.
   NOTE: .mv also appears in the <=960 stacking rule further down
   (.split,.contact-grid,.mv,.band.split) - that selector is still live for its
   other three, so do not strip .mv out of it as "dead" without re-reading this.
   Do not "clean this up". */
.mv{display:grid;grid-template-columns:1fr 1fr;gap:26px}
.mv .card{border-top:4px solid var(--ink)}

/* ---------- Market segments (hover inversion) ---------- */
.seg-grid{gap:0;border:2px solid var(--ink)}
.seg{border:0;outline:1px solid var(--ink);padding:40px;display:block;transition:background .15s}
.seg svg{width:52px;height:52px;color:var(--steel);margin-bottom:18px}
.seg h3{transition:color .15s}
.seg p{margin-top:8px}
.seg:hover{background:var(--navy)}
.seg:hover h3{color:#fff}
.seg:hover svg{color:#fff}
.seg:hover p{color:#aeb9c2}

/* ---------- Steps (rentals) ----------
   NO CONSUMER as of 2026-08-04: rebar.html was the only page using .step, and
   its 01/02/03 splicing cards were folded into that section's body paragraph
   as prose. Kept deliberately (owner), same call as .split-pair - the numbered
   pattern is likely to return. Do not "clean this up". */
.step{border-top:4px solid var(--ink);padding-top:22px}
/* #b9c2c9 measured 1.63:1 on --concrete. These numerals are visible text, not
   aria-hidden, so they need 3:1 (they stay large-bold at every breakpoint:
   41.6px here, 28px in the <=680 block, both over the 18.66px bold threshold).
   #78858f = 3.41:1 on --concrete, 3.78:1 on white. */
.step-n{font-family:var(--head);font-weight:700;font-size:2.6rem;color:#78858f;line-height:1;display:block;margin-bottom:10px}
.step h3{margin-bottom:8px}

/* ---------- Blog ---------- */
.post{border:1px solid var(--line);background:var(--paper)}
.post>img{object-fit:cover;object-position:center}
.post .body{padding:26px}
.post .date{color:var(--steel);font-family:var(--head);font-weight:600;font-size:.85rem;
  text-transform:uppercase;letter-spacing:.1em}
.post h3{margin:8px 0;font-size:1.25rem}

/* ---------- Contact ---------- */
.contact-grid{display:grid;grid-template-columns:1fr 1fr;gap:56px;align-items:start}
.info-row{display:flex;gap:16px;margin-bottom:22px}
.info-row .ic{width:46px;height:46px;flex:none;background:var(--concrete);color:var(--ink);
  display:grid;place-items:center}
.info-row .ic svg{width:22px;height:22px}
.info-row strong{font-family:var(--head);text-transform:uppercase;letter-spacing:.05em}
.form{background:var(--paper);border:1px solid var(--line);padding:34px}
.field{margin-bottom:18px}
.field label{display:block;font-family:var(--head);font-weight:600;font-size:.9rem;
  text-transform:uppercase;letter-spacing:.06em;margin-bottom:6px}
.field input,.field textarea{
  width:100%;padding:12px 14px;border:1px solid var(--line);
  font-family:var(--body);font-size:1rem;background:var(--paper);
}
.field input:focus,.field textarea:focus{outline:2px solid var(--blue);border-color:transparent}
.two{display:grid;grid-template-columns:1fr 1fr;gap:16px}

/* ---------- CTA - ink band (blue stays on the button) ---------- */
.cta{background:var(--ink);color:#fff;text-align:center;padding:72px 24px}
.cta h2{color:#fff}
.cta p{color:#c6cfd6;max-width:560px;margin:14px auto 28px}

/* ---------- Seal & marks ---------- */
.seal-lg{width:100%;max-width:440px;margin:0 auto;display:block}
.beam-figure{margin:0}
.beam-figure img{width:100%;height:auto;display:block}
.beam-figure figcaption{font-family:var(--head);font-weight:600;text-transform:uppercase;
  letter-spacing:.08em;font-size:.95rem;color:var(--muted);margin-top:16px;text-align:center}
/* Project gallery figures (captioned) */
.pgal figure{margin:0}
.pgal img{width:100%;aspect-ratio:4/3;object-fit:cover;display:block}
.pgal figure{cursor:pointer}
.pgal figcaption{font-family:var(--head);font-weight:600;text-transform:uppercase;
  letter-spacing:.06em;font-size:.9rem;color:var(--muted);margin-top:8px}
/* ---------- Project gallery lightbox (hard edges, no animation) ---------- */
.lbx{position:fixed;inset:0;z-index:200;background:rgba(34,30,31,.92);
  display:flex;flex-direction:column;align-items:center;justify-content:center}
.lbx[hidden]{display:none}
.lbx-img{max-width:min(1200px,86vw);max-height:76vh;width:auto;height:auto;display:block}
.lbx-bar{max-width:min(1200px,86vw);width:100%;display:flex;gap:18px;
  justify-content:space-between;align-items:baseline;margin-top:14px}
.lbx-cap{font-family:var(--head);font-weight:600;text-transform:uppercase;
  letter-spacing:.06em;font-size:1.05rem;color:#d5dbe0}
.lbx-count{font-family:var(--head);font-weight:600;color:#8b969e;white-space:nowrap}
.lbx button{background:none;border:2px solid transparent;color:#fff;cursor:pointer;
  padding:10px;display:grid;place-items:center}
.lbx button:hover svg,.lbx button:focus-visible svg{color:var(--blue)}
.lbx button:focus-visible{border-color:var(--blue);outline:none}
.lbx svg{width:34px;height:34px;color:#fff}
.lbx-x{position:absolute;top:18px;right:22px}
.lbx-prev,.lbx-next{position:absolute;top:50%;transform:translateY(-50%)}
.lbx-prev{left:14px}.lbx-next{right:14px}
@media(max-width:767px){.lbx-img{max-width:94vw;max-height:64vh}
  .lbx-prev{left:2px}.lbx-next{right:2px}}
/* ---------- Prev/next project nav (bottom of project pages) ----------
   3-col grid keeps the center pair truly centered while prev/next pin to
   the container's outer edges; long names get ellipsis truncation.
   Below 767px the destination NAME swaps for generic Prev/Next labels. */
.pnav{display:grid;grid-template-columns:minmax(0,1fr) auto minmax(0,1fr);
  gap:14px;align-items:center;margin-top:44px}
.pnav-side{display:inline-flex;align-items:center;gap:10px;min-width:0;max-width:100%;
  padding-left:18px;padding-right:18px}
.pnav-prev{justify-self:start}
.pnav-next{justify-self:end}
.pnav-side svg{width:16px;height:16px;flex:none}
.pnav-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* .pnav-gen needs the SAME clamp as .pnav-name. Without it the generic
   "Prev Project"/"Next Project" label overflowed its own button border at 320px
   (measured 11.2px past the edge, into the sibling button) because nowrap had
   nothing to fall back to. min-width:0 lets it shrink as a flex item; it is
   blockified by the flex parent, so text-overflow applies despite display:inline. */
.pnav-gen{display:none;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}
.pnav-center{display:flex;gap:14px;flex-wrap:wrap;justify-content:center}
/* 960 here is NOT linked to the header's 960 mobile-nav breakpoint: the
   matching number is coincidence (owner note 2026-07-17). This one exists
   because a two-row generic-label bar reads better than a hard ellipsis on
   tablet. Re-measure independently; do not "unify" them. */
@media(max-width:960px){
  .pnav{grid-template-columns:1fr 1fr;gap:10px}
  .pnav-prev{grid-column:1;grid-row:1}
  .pnav-next{grid-column:2;grid-row:1}
  .pnav-center{grid-column:1 / -1;grid-row:2}
  .pnav-side{padding:12px 14px}
  .pnav-name{display:none}
  .pnav-gen{display:inline}
}
/* The .seal-watermark ghost seal (620px, 4% opacity, bled off the footer's
   bottom-right) was REMOVED 2026-08-04 (owner) along with its rule; the large
   seal in the footer's first grid column replaces it as the brand mark. The
   .site-footer .container z-index below is kept: .site-footer is still
   position:relative and the stacking context costs nothing. */
.site-footer .container{position:relative;z-index:2}
/* Was display:flex when this row held the seal beside the address list. The
   seal is gone (see the FOOTER constant), so the row has ONE child and the flex
   context did nothing but shrink-wrap the <ul> to its content width. */
.addr-row{display:block}
/* Footer seal, first grid column. Replaces BOTH the ghost watermark and the
   small panel-backed seal that used to sit in the address row.
   FILE, 2026-08-06 (owner). Swapped TWICE the same day, and the order matters
   if you read the history: edc-seal.png (a bare cutout) -> edc-seal-2048.png (a
   PHOTOGRAPH of the physical sign) -> assets/edc-seal-white.webp, the clean
   white-circle rendering that is live now. The photograph is superseded because
   it carried the sign's chipped rim, a scuff, uneven cream paper and a 2.75deg
   tilt. It is still on disk and still referenced nowhere.
   THE LIGHT-PANEL RULING IS UNCHANGED AND STILL HONOURED HERE. The 2026-08-03
   panel was declined and the 2026-08-04 note said do not add background or
   padding to rescue the contrast. Nothing was added: the disc is part of the
   ARTWORK. There is still no container, no background, no padding and no radius
   on this element. Do not "restore" a panel here on the strength of the disc -
   there is nothing to restore. CLAUDE.md's "never a plate behind the seal" rule
   is intact.
   THE 1.38:1 CONTRAST FAILURE IS RETIRED, not worked around. The old cutout put
   a bare #221E1F ring on transparency, compositing to 1.38:1 on --navy. The
   white disc measures 11.27:1 (the cream photo was 9.18:1), past the 3:1 WCAG
   wants for a graphic. edc-seal-reversed.png (10.34:1) is also still unreferenced.
   FIRST WEBP ON THE SITE. q90, 192 KB against the photograph's 3.51 MB, with
   alpha preserved bit-exactly and no banding (white disc 59 tone levels with a
   largest gap of 1, blue ring 155 / gap 2). build_nav's _IMG_PATH already
   matched .webp, so the stamper needed no change. PNG-8 was measured first and
   REJECTED: it collapsed the disc to 10 levels with a gap of 13, which contours.
   SIZING - 168px is deliberate and is NOT interchangeable with the old 160.
   Sizes here are matched on the MARK, not on the box: the printed mark fills
   101.4% of the original cutout's width (tight, very slightly clipped) but only
   93.8% of this file, which carries a white margin. So an identical box would
   render the mark 7.1% smaller. 168px reproduces the 158.7px mark the old
   156.6px box produced. Same reason the 960 and 680 caps went 120 -> 129 and
   96 -> 103, and why the seal track went .9fr -> .98fr (at 1200 the .9fr track
   resolved to 156.6px, so the cap alone could never have reached 168).
   The 93.8% here vs the photograph's 94.2% is a 0.4% difference, i.e. the mark
   reads 157.6px where it read 158.2px - invisible, so the box did NOT move.
   168px against a 2048px source is a 12.2x downscale, 4.05x at DPR 3 - it never
   upscales. Square, no radius, no container. */
.f-sealcol{display:flex;align-items:flex-start}
.f-seal{width:100%;max-width:168px;height:auto}
.divider-mark{display:block;margin:0 auto;height:84px;width:auto}

/* ---------- Curated social videos (facade embeds; see js/social.js) ---------- */
.sv-row{display:grid;grid-template-columns:repeat(4,1fr);gap:26px}
.sv-card{position:relative;aspect-ratio:9/16;background:var(--ink);overflow:hidden;cursor:pointer}
.sv-card.is-todo{cursor:default}
.sv-poster{width:100%;height:100%;object-fit:cover;object-position:center;display:block}
.sv-card.is-todo .sv-poster{opacity:.55}
.sv-badge{position:absolute;top:0;left:0;z-index:3;background:var(--ink);color:#fff;
  font-family:var(--head);font-weight:600;font-size:.75rem;letter-spacing:.1em;
  text-transform:uppercase;padding:6px 12px}
.sv-play{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);z-index:2;
  width:64px;height:64px;background:var(--blue-button);color:#fff;border:0;cursor:pointer;
  display:grid;place-items:center;transition:background .15s}
.sv-play svg{width:30px;height:30px}
.sv-card:hover .sv-play{background:var(--blue)}
.sv-soon{position:absolute;left:0;right:0;bottom:0;z-index:2;background:rgba(20,23,26,.85);
  color:#c6cfd6;font-family:var(--head);font-weight:600;font-size:.8rem;
  letter-spacing:.12em;text-transform:uppercase;text-align:center;padding:10px 0}
.sv-caption{position:absolute;left:0;right:0;bottom:0;z-index:2;
  background:linear-gradient(to top,rgba(20,23,26,.9),transparent);color:#fff;
  font-size:.85rem;padding:26px 12px 10px}
.sv-frame{position:absolute;inset:0 0 40px 0;width:100%;height:calc(100% - 40px);border:0;z-index:2}
.sv-out{position:absolute;left:0;right:0;bottom:0;height:40px;z-index:3;
  display:flex;align-items:center;justify-content:center;background:var(--navy);color:#fff;
  font-family:var(--head);font-weight:600;font-size:.8rem;letter-spacing:.08em;text-transform:uppercase}
.sv-link-card{display:block;width:100%;height:100%}
@media (prefers-reduced-motion:reduce){.sv-play{transition:none}}
@media(max-width:960px){.sv-row{grid-template-columns:1fr 1fr}}
@media(max-width:680px){
  .sv-row{display:flex;overflow-x:auto;scroll-snap-type:x mandatory;
    -webkit-overflow-scrolling:touch;gap:14px;padding-bottom:10px}
  .sv-card{flex:0 0 76%;scroll-snap-align:center}
}

/* product art from the brochure PDFs: shown contained at native size, never
   stretched to fill (upscaling brochure renders makes them mushy) */
.card .prod-img{display:block;width:100%;height:auto;margin:0 auto 18px}
/* Uniform product-card image cell (owner order 2026-07-16): fixed height,
   image centered inside at native-or-smaller size. max-width/max-height
   shrink oversized art to fit but NEVER upscale small art (which
   object-fit:contain on the img itself would do). */
.prod-cell{height:300px;display:flex;align-items:center;justify-content:center;
  margin-bottom:18px}
.prod-cell img{max-width:100%;max-height:100%;width:auto;height:auto;display:block}

/* ---------- Catalog grid ---------- */
.cat-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:26px}
.cat-item{border:1px solid var(--line);background:var(--paper);display:block;transition:border-color .15s}
.cat-item .cover{display:grid;place-items:center;aspect-ratio:3/4;background:var(--concrete);
  padding:20px;border-bottom:1px solid var(--line)}
.cat-item .cover img{max-width:100%;max-height:100%;object-fit:contain;box-shadow:0 6px 18px rgba(10,61,98,.18)}
.cat-item .cat-name{display:block;padding:16px 18px;font-family:var(--head);font-weight:600;
  font-size:1.3rem;line-height:1.15;text-transform:uppercase;letter-spacing:.05em;color:var(--ink)}
.cat-item .cat-desc{display:block;padding:0 18px 16px;margin-top:-4px;font-size:.95rem;
  line-height:1.5;color:var(--charcoal)}
.cat-item:hover{border-color:var(--blue)}
.cat-item:hover .cat-name{color:var(--blue)}
@media(max-width:960px){.cat-grid{grid-template-columns:1fr 1fr}}
@media(max-width:680px){.cat-grid{grid-template-columns:1fr}}

/* ---------- Video plates ---------- */
.video-plate{background:var(--navy)}
.video-plate h2{color:#fff}
.video-plate .eyebrow{color:#9fc3da}
.video-plate video,.video-plate .vid-poster{width:100%;display:block;margin-top:32px}
.vid-caption{color:#c9d8e2;margin-top:14px;font-size:.95rem}
/* Press release masthead marks (stendeck.html). BOTH live here rather than in
   style attributes because the row steps down twice on narrow viewports and an
   inline size would beat the media queries.
   Scaled 1.75x on 2026-08-06 (owner): seal 98 -> 171.5, wordmark 26 -> 45.5.
   That is the second 1.75x for the seal, which ran at 56 before that day.
   Neither upscales. The seal is a 1098px PNG at 171.5 = 3.20x at DPR 2 and
   2.13x at DPR 3. The wordmark is an SVG (viewBox 540.59 x 70.01), so it is
   resolution-independent at any size; its 7.7286 aspect ratio makes the 45.5px
   height 351.6px wide, and width:auto keeps it from ever being stretched.
   THE WORDMARK IS THE WIDE ONE - 351.6 against the seal's 171.5. It, not the
   seal, is what sets every breakpoint below. Read the two step blocks near the
   bottom of this file before resizing either mark again. */
.pr-seal{width:171.5px;height:auto}
.pr-word{height:45.5px;width:auto}

/* ---------- Project detail ---------- */
.pdetail{display:grid;grid-template-columns:1.7fr 1fr;gap:48px;align-items:start}
.pdetail .hero-img{width:100%;margin-bottom:32px;aspect-ratio:16/10;object-fit:cover;object-position:center}
.facts{position:sticky;top:136px}
.facts .row{display:flex;justify-content:space-between;gap:18px;padding:12px 0;
  border-bottom:1px solid var(--line)}
.facts .row:last-child{border-bottom:0;padding-bottom:0}
.facts .k{font-family:var(--head);font-weight:600;text-transform:uppercase;
  letter-spacing:.04em;color:var(--ink)}
.facts .v{color:var(--muted);text-align:right}

/* ---------- Footer - ink, dual hours ---------- */
.site-footer{background:var(--navy);color:#b6c0c8;padding:72px 0 0;position:relative;overflow:clip}
/* FIVE tracks since 2026-08-04: a .9fr seal column was added AHEAD of the four
   existing ones rather than putting the seal inside the description column, so
   the description keeps its own full track and is not crowded. The four
   original fractions are unchanged relative to each other (1.4/1/1/1.4), so the
   link and hours columns keep their proportions; every track just resolves
   narrower because there is one more of them. */
/* First track was .9fr from 2026-08-04 to 2026-08-06; widened to .98fr so the
   seal box reaches 168px at 1200 (it resolved to 156.6px at .9fr, below the
   cap, so raising max-width alone did nothing). See the .f-seal note for why
   168 and not 160. It takes 11.6px from the other four tracks combined. */
.footer-grid{display:grid;grid-template-columns:.98fr 1.4fr 1fr 1fr 1.4fr;gap:40px}
.site-footer img.flogo{height:60px;background:#fff;padding:8px 12px;margin-bottom:18px}
/* h3, not h4: the footer columns followed the page's last h2 with no h3 between,
   skipping a level (WCAG 1.3.1). The selector MUST move with the tag - keyed to
   h4 it would silently drop the white-on-navy colour and sizing. */
.site-footer h3{color:#fff;font-size:1.02rem;margin-bottom:16px}
.site-footer ul{list-style:none}
.site-footer li{margin-bottom:10px}
.site-footer a:hover{color:#fff}
.social{display:flex;gap:12px;margin-top:18px}
.social a{width:40px;height:40px;background:rgba(255,255,255,.08);display:grid;
  place-items:center;color:#fff}
.social a:hover{background:rgba(255,255,255,.2)}
.social a svg{height:18px;width:auto}
.hours li{display:flex;justify-content:space-between;gap:16px;
  border-bottom:1px solid rgba(255,255,255,.12);padding:9px 0;margin:0}
/* Label column FIXED at 88px so OFFICE and YARD / WAREHOUSE hours start at the
   same x (2026-08-06, owner). Both spans used to be shrink-to-fit, so flex
   handed each row a different label width and the two "Mon-Fri" strings began
   43.5px apart at 1440. justify-content:space-between above is now inert - the
   value takes all remaining space - but it is left in place as the fallback if
   the flex values below are ever removed.
   88px IS NOT ARBITRARY. Measured window is [83, 94]:
     floor 82.6px = "WAREHOUSE" at 17px/600/.05em. Below it the label overflows.
     ceiling 94px = at 1080 the footer column is 207.6px, so anything wider
     leaves the hours under ~97px and they break to a THIRD line (96px does).
   88 sits mid-window with ~5px of slack each way. Re-measure both bounds before
   changing the label text, the font, or the 16px gap.
   KNOWN, ACCEPTED: between ~961 and ~1053 the hours run to three lines. The
   column there is 178-194px and 82.6 + 16 + 97 = 195.6 does not fit at any
   label width, so alignment is not what breaks it - at 961 the YARD hours
   already wrapped to three lines before this change. */
.hours li span:first-child{flex:0 0 88px;font-family:var(--head);font-weight:600;
  text-transform:uppercase;letter-spacing:.05em;color:#fff}
.hours li span:last-child{flex:1 1 auto}
.hours-tel{border-bottom:0!important;padding-top:16px!important}
.hours-tel a{font-family:var(--head);font-size:1.6rem;font-weight:700;color:#fff;
  letter-spacing:.04em}
/* #8b969e measured 3.75:1 on --navy (AA needs 4.5). #a0adb6 = 4.93:1, the
   minimum lift that clears AA while staying dimmer than the footer's own
   #b6c0c8 body text so the copyright line keeps its lower visual weight. */
.footer-bottom{border-top:1px solid rgba(255,255,255,.12);margin-top:52px;padding:22px 0;
  text-align:center;font-size:.9rem;color:#a0adb6}

/* ---------- Skip link (keyboard: first tab stop, jumps to #main) ---------- */
.skip-link{position:absolute;left:8px;top:-56px;z-index:120;
  background:var(--blue-button);color:#fff;font-family:var(--head);font-weight:600;
  text-transform:uppercase;letter-spacing:.04em;font-size:.9rem;padding:12px 18px;
  transition:top .15s ease}
.skip-link:focus{top:8px}

/* ---------- WCAG 2.2 target size: grow the HIT AREA only, via padding, so
   the visible text/size is unchanged (same technique as .hero-dots). Footer
   quick-links/products/address links and contact info links were 17-20px;
   the added padding lifts each to >=24px. Margins trimmed to keep vertical
   rhythm near-identical. .hours rows and .social (already >=24) untouched. */
.site-footer ul:not(.hours) a{display:inline-block;padding:2px 0}
.site-footer ul:not(.hours) li{margin-bottom:8px}
.crumbs a{display:inline-block;padding:4px 0}
.info-row a{display:inline-block;padding:3px 0}

/* ---------- Responsive ---------- */
/* PAGE-LAYOUT grids switch at 960 (unchanged). Deliberately SEPARATE from the
   nav breakpoint (1140, below): raising the hamburger point must never reflow
   the page columns, and this is also unrelated to the pnav 960 breakpoint. */
@media(max-width:960px){
  .split,.contact-grid,.mv,.band.split{grid-template-columns:1fr;gap:36px}
  .band{padding:44px 30px}
  .split.reverse .split-text{order:0}
  .cols-3,.project-grid{grid-template-columns:1fr 1fr}
  .stats .grid{grid-template-columns:1fr 1fr;gap:32px}
  .stat{border-left:0}
  .partner-grid{grid-template-columns:repeat(3,1fr);gap:26px}
  /* Two columns: the seal takes the first cell and the description sits beside
     it, so the seal comes down to 120px - at the desktop 160px it would outweigh
     the text it is paired with in a half-width cell. */
  .footer-grid{grid-template-columns:1fr 1fr}
  .f-seal{max-width:129px}
  .filter-bar{flex-wrap:wrap;width:auto}
  .seg-grid.cols-2{grid-template-columns:1fr}
}
@media(max-width:1139px){
  /* ---- FIXED TOP UNIT (2026-07-20; nav breakpoint raised 960->1079 on
     2026-07-23, then 1079->1139 on 2026-08-10 for the brand seal) ----
     Single source of truth: --util-h and --header-h. Header height, util
     height, body padding, panel top offset, and panel max-height ALL derive
     from these two vars. Change a var, all five move together. The only
     other number in the calcs is the header's 1px border-bottom.
     This unit + the hamburger share ONE breakpoint (the fixed panel is
     positioned off the fixed header); desktop (>=1140px) keeps its sticky
     header. */
  :root{--util-h:40px;--header-h:108px}
  .util-bar{position:fixed;top:0;left:0;right:0;z-index:51;height:var(--util-h)}
  .util-in{height:100%;padding-top:0;padding-bottom:0}
  .site-header{position:fixed;top:var(--util-h);left:0;right:0}
  .nav{height:var(--header-h)}
  body{padding-top:calc(var(--util-h) + var(--header-h) + 1px)}
  /* anchor targets must clear the fixed chrome on hash navigation */
  [id]{scroll-margin-top:calc(var(--util-h) + var(--header-h) + 12px)}
  /* project-page facts sidebar (sticky below the breakpoint) clears it too */
  .facts{top:calc(var(--util-h) + var(--header-h) + 16px)}
  /* mobile nav engages below 1140 (was 1080, 960 before): the 8-link nav plus
     the brand seal needs ~1102px of layout width, and 1140 is the first
     viewport that clears it with the 15px scrollbar out */
  .nav-links{
    position:fixed;inset:calc(var(--util-h) + var(--header-h) + 1px) 0 auto 0;
    background:var(--paper);flex-direction:column;
    align-items:stretch;gap:0;padding:10px 24px 24px;border-bottom:1px solid var(--line);
    transform:translateY(-140%);transition:transform .25s;z-index:49;
  }
  .nav-links.open{transform:translateY(0)}
  .nav-links li{width:100%;border-bottom:1px solid var(--line)}
  .nav-links a{display:block;padding:14px 0}
  /* the WHOLE row toggles: parent <a> (desktop hover trigger) swaps out for
     the sibling <button>. CSS-only swap - same markup serves both widths. */
  .has-drop>a,.has-fly>a{display:none}
  .drop-toggle{display:flex;align-items:center;justify-content:space-between;gap:10px;
    width:100%;padding:14px 0;background:none;border:0;cursor:pointer;text-align:left;
    font-family:var(--head);font-weight:600;font-size:1.02rem;letter-spacing:.05em;
    text-transform:uppercase;color:var(--ink)}
  .drop-toggle::after{content:"";width:9px;height:9px;flex:none;margin-right:6px;
    border-right:2px solid var(--blue);border-bottom:2px solid var(--blue);
    transform:rotate(45deg);transition:transform .2s}
  .drop-toggle[aria-expanded="true"]::after{transform:rotate(225deg)}
  /* nested toggle (Post Shores) reads as a dropdown row, not a top-level one */
  .dropdown .drop-toggle{font-family:var(--body);font-weight:500;font-size:.92rem;
    letter-spacing:.02em;text-transform:none;padding:13px 18px}
  .m-only{display:block}
  /* collapsed by default; aria-expanded on the sibling button is the state.
     max-height is the ONE animated property. visibility is not a second
     animation: it flips after the height finishes purely so collapsed links
     drop out of tab order and AT (vertical padding must stay 0 - border-box
     floors a box's height at its padding, which would leave a visible sliver). */
  .dropdown,.flyout{position:static;opacity:1;border:0;border-left:2px solid var(--line);
    min-width:auto;padding:0 0 0 14px;max-height:0;overflow:hidden;visibility:hidden;
    transition:max-height .25s ease,visibility 0s .25s}
  .drop-toggle[aria-expanded="true"]+.dropdown,
  .drop-toggle[aria-expanded="true"]+.flyout{max-height:640px;visibility:visible;
    transition:max-height .25s ease}
  .dropdown>li:last-child,.flyout>li:last-child{border-bottom:0}
  .menu-toggle{display:block}
  /* scroll lock while the panel is open (class lands on <html>); gated to
     mobile widths so a leftover class can never lock a desktop viewport */
  .nav-lock,.nav-lock body{overflow:hidden}
}
@media(max-width:680px){
  /* header shrinks: ONE var change moves height, body padding, panel top
     offset and panel max-height together (see the 960 block) */
  :root{--header-h:88px}
  .brand img{height:34px}
  .cols-3,.project-grid,.two,.cols-2,.footer-grid,.partner-grid{grid-template-columns:1fr}
  .coupler-pair{grid-template-columns:1fr;gap:18px}
  .hero-inner{padding:64px 0 72px}
  .section{padding:48px 0}
  .pdetail{grid-template-columns:1fr}
  .facts{position:static}
  .util-in{justify-content:center;gap:20px}
}

/* =========================================================
   MOBILE PASS (2026-07-16). Every rule below sits inside a
   media query; desktop above the breakpoints is untouched.
   ========================================================= */
@media(max-width:1139px){
  /* a fully expanded panel can be taller than a phone screen: it scrolls
     within itself, capped at viewport minus the fixed chrome (dvh line
     wins on browsers that support it; vh is the fallback) */
  .nav-links{max-height:calc(100vh - var(--util-h) - var(--header-h) - 1px);
    max-height:calc(100dvh - var(--util-h) - var(--header-h) - 1px);
    overflow-y:auto;overscroll-behavior:contain}
  /* 44px tap targets */
  .menu-toggle{min-width:44px;min-height:44px;line-height:1}
  .dropdown a,.flyout a{padding:13px 18px}
}
@media(max-width:680px){
  /* .nav-links max-height auto-tracks the --header-h change (960 block) */
  .social a{width:44px;height:44px}
  .util-tel{display:inline-block;padding:8px 0}
}
/* True phone widths. The header row (34px wordmark ~210px + nowrap CTA
   ~180px + toggle) has ~500px of unshrinkable min-content and forced
   sitewide horizontal overflow at 375. The header CTA collapses into the
   menu (Contact) and the utility-bar phone; the hero and the quote box on
   every page still carry Request a Quote. */
/* =========================================================
   MOBILE SIZING PASS (2026-07-20, owner-approved scale).
   Every rule sits inside a media query capped at 960px;
   desktop >=961px is untouched.
   ========================================================= */
/* the forgotten band: 681-960 previously kept full desktop rhythm */
@media(min-width:681px) and (max-width:960px){
  .section{padding:72px 0}
  .hero-inner{padding:96px 0 104px}
  .page-banner{padding:28px 0}
  :root{--banner-h:388px}   /* tallest content in this band is 325px (STENdeck at 960) */
  /* Back to the wide plate: the desktop 20% cap is 154-192px here, and the
     text cannot live in that - at 167px (the widest that clears man 1 at 768)
     the plate renders 506px tall inside a 388px banner and loses its top.
     So the plate stays wide on tablet and still crosses the crew. Not fixable
     by narrowing; it needs a different arrangement at this size. */
  .page-banner--crew .plate{max-width:600px;padding:42px 52px}
}
@media(max-width:680px){
  /* type scale */
  h1{font-size:2rem}                       /* 32px hero headline */
  .page-banner h1{font-size:1.75rem}       /* 28px: subpage title sits below the hero */
  h2{font-size:1.625rem}                   /* 26px */
  h3{font-size:1.3125rem}                  /* 21px */
  .project h3{font-size:1.1875rem}         /* 19px */
  .stat .num{font-size:2.25rem}            /* 36px */
  .step-n{font-size:1.75rem}               /* 28px */
  .eyebrow,.kicker{font-size:.8125rem;letter-spacing:.14em}
  .hero p{font-size:1.0625rem}             /* 17px, matches body copy */
  .btn{font-size:.9375rem;padding:12px 22px}
  .util-bar,.util-tel{font-size:.8125rem}  /* 13px, one row in the fixed 40px bar */
  .hours-tel a{font-size:1.375rem}         /* 22px */
  .cat-item .cat-name{font-size:1.125rem}  /* 18px */
  /* spacing scale (.section and .hero-inner are set in the 680 block above) */
  .page-banner{padding:16px 0}
  :root{--banner-h:344px}   /* tallest content in this band is 309px (STENdeck at 320) */
  /* Same as the tablet block: 20% is 75px at 375, and man 1's hat starts 58px
     in, so nothing usable fits beside him. Plate stays full width. It only
     grazes the chin line here (plate top 147, lowest chin 162), so this is the
     least broken of the three bands. */
  .page-banner--crew .plate{max-width:600px;padding:28px 24px}
  .cta{padding:48px 24px}
  .card{padding:24px}
  .card .thumb{margin:-24px -24px 24px}
  .photo-band{min-height:380px}
  .photo-band .plate{padding:28px 24px}
  /* The desktop 96/34 pair needs 385px and the plate has 327px of content width
     at 375, so both marks step down here. 68/26 needs 294.9 and fits. The seal
     still lands larger than the 52 it was before 2026-08-06. See the
     .plate-marks note above for why the 2:1 ratio no longer holds. */
  .photo-band .plate-marks .mark-seal{height:68px}
  .photo-band .plate-marks .mark-word{height:26px}
  .site-footer{padding:48px 0 0}
  .footer-bottom{margin-top:32px}
  .footer-grid{gap:28px}
  /* Single column here (see the 680 block above), so the seal is the first
     thing in the stacked footer. 96px keeps it a mark rather than a banner -
     at 160px it would fill most of a 375px viewport's width. */
  .f-seal{max-width:103px}
  .grid,.project-grid,.cat-grid{gap:18px}
}
/* True phone widths (see header comment below) */
@media(max-width:560px){
  .brand img{height:28px}
  .nav{gap:12px}
  .nav-cta{gap:8px}
  .nav-cta .btn{display:none}
}
/* Press release masthead, narrow phones. A FOURTH breakpoint, deliberately not
   folded into 680/560: this is one row on one page and the number comes from
   that row's arithmetic, not from the page layout.

   THE LADDER, and why each number is what it is. The row is
   `seal + 24 gap + wordmark` inside a container that gives vw - 48:

     >=681   seal 171.5  word 351.6 (h 45.5)   row 547.1
     371-680 seal 98     word 200.9 (h 26)     row 322.9
     <=370   seal 68     word 200.9 (h 26)     row 292.9

   At full size the row needs 547.1, so it stops fitting the padding below vw
   595.1 and would scroll the document below 571.1. The first step therefore
   has to fire well above that, and 680 is used because it ALREADY EXISTS as
   this file's mobile breakpoint - no new number was invented for it. At 681 the
   container gives at least 618 even with a 15px scrollbar, against the 547.1
   the row needs, so the step lands with ~70px to spare rather than on a cliff.
   The 371-680 rung is exactly the pair the masthead ran at before this scaling.

   370 is the second rung: at 98/26 the row needs 322.9, which stops fitting the
   padding below vw 370.9 and scrolls below 346.9.
   68px is the largest seal that still clears 320: the right edge lands at 316.9
   in a 320 viewport, 3.1px clear. The ceiling is 71.1 and it is set by the
   WORDMARK, not the seal - at 320 the wordmark plus its gap alone eats 224.9 of
   the 272px of content width. Anything above 68 here reintroduces the scroll.
   NOTE 320 never did fit inside the container padding, not even at the old
   56px seal (that needed vw >= 328.9). Encroaching on the padding at 320 is the
   historical behaviour; the document-level scroll is the thing that must not
   come back. Do not "finish the job" by shrinking the seal to ~47px to win back
   the padding - that lands it smaller than it was before the owner asked for it
   to grow, twice. Shrink the WORDMARK if that padding is ever wanted. */
@media(max-width:680px){
  .pr-seal{width:98px}
  .pr-word{height:26px}
}
/* MUST stay after the 680 block: same specificity, so source order decides
   which one wins at widths where both match. */
@media(max-width:370px){
  .pr-seal{width:68px}
}
