/* ============================================================
   NW Contractor Services Ltd — site styles
   Hand-built static site. Edit this file directly.
   ============================================================ */

/* --- Self-hosted Manrope (variable font, weights 400–800) --- */
@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url('/assets/fonts/manrope-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
    U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212,
    U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url('/assets/fonts/manrope-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
    U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* --- Motion tokens (single motion language for the whole site) --- */
:root{
  --dur:.6s;                                /* reveal duration */
  --ease:cubic-bezier(.22,.61,.36,1);       /* shared easing */
  --rise:18px;                              /* reveal rise distance */
  --hover-dur:.25s;                         /* hover transition */
  --draw-dur:1.1s;                          /* line draw-in */
  --stagger:90ms;                           /* choreography step (read by site.js) */
  --dur-press:.12s;                         /* press/tap settle */
  --dur-mark:.35s;                          /* small marks & confirmations */
  --ease-spring:cubic-bezier(.34,1.56,.64,1); /* sparing overshoot — press-release only */
}

/* --- Base --- */
*,*::before,*::after{box-sizing:border-box}
body{
  margin:0;
  background:#FBFBF9;
  color:#111;
  font-family:Manrope,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
  -webkit-font-smoothing:antialiased;
  text-rendering:optimizeLegibility;
}
img{max-width:100%;display:block}

/* --- Page link reset (mirrors prototype .nwcs-page a) --- */
.nwcs-page a{text-decoration:none}

/* --- Anchor jumps (Batch 3) ------------------------------------------------
   The one navigation motion the estate lacked: an in-page jump glides instead
   of teleporting. Motion only where motion is wanted; the offset is
   unconditional because it is layout truth, not animation — 61px sticky header
   + 16px breath, so a jump target never lands underneath the header. */
@media (prefers-reduced-motion: no-preference){
  html{scroll-behavior:smooth}
}
#main,#main [id]{scroll-margin-top:77px}

/* --- Scroll-reveal --- */
.rv{transition:opacity var(--dur) var(--ease),transform var(--dur) var(--ease)}
/* Pre-reveal state is gated on :not(.on) — the site's own idiom (.how-step,
   .stn-card, .stn-eyebrow all do this). Group 2 spec-integrity fix A: it used
   to be `.js .rv{opacity:0;transform:…}` neutralised by an inline
   `transform:none` the observer wrote and never cleared, which left every
   revealed card with an inline transform that hover/press rules could not beat.
   Now .on simply stops the pre-state matching, so no inline transform is
   needed and the card lift/press transforms win on their own specificity. */
.js .rv:not(.on){opacity:0;transform:translateY(var(--rise))}
@media (prefers-reduced-motion: reduce){
  .rv{opacity:1 !important;transform:none !important;transition:none !important}
}

/* ============================================================
   Hero art — engineering cross-section (strata / rebar / grid)
   Decorative; sits behind hero text. Draws itself on load;
   reduced-motion and no-JS render the composed end-state.
   ============================================================ */
.hero-art{position:absolute;inset:0;pointer-events:none;overflow:hidden}
.hero-art svg{width:100%;height:100%;display:block}
.hero-art [data-depth]{will-change:transform}

/* Draw-in: pathLength="1" lets one rule cover every line */
.js .hero-art .d{
  stroke-dasharray:1;
  stroke-dashoffset:1;
  animation:hero-draw var(--draw-dur) var(--ease) forwards;
  animation-delay:calc(var(--i,0)*var(--stagger));
}
.js .hero-art .f{
  opacity:0;
  animation:hero-fade .8s var(--ease) forwards;
  animation-delay:calc(var(--i,0)*var(--stagger));
}
@keyframes hero-draw{to{stroke-dashoffset:0}}
@keyframes hero-fade{to{opacity:var(--o,1)}}

@media (prefers-reduced-motion: reduce){
  .js .hero-art .d{animation:none;stroke-dashoffset:0}
  .js .hero-art .f{animation:none;opacity:var(--o,1)}
  .hero-art [data-depth]{transform:none !important;will-change:auto}
}

/* ============================================================
   "Take-off → final account" — five-step connector choreography
   Each step carries a segment of one continuous line that deepens
   from step 01 to solid accent green at step 05 (the close-out dot).
   Composed end-state is the default; .js + .on drive the draw.
   ============================================================ */
.how-step{position:relative}
.how-step::before{
  content:"";position:absolute;top:-1px;left:16px;right:16px;height:2px;
  background:#2E6A4C;opacity:var(--seg-o,.4);border-radius:2px;
  transform-origin:left center;
  transition:transform .6s var(--ease);
  transition-delay:calc(var(--d,0ms) + 160ms);
}
.js .how-step:not(.on)::before{transform:scaleX(0)}
/* Final-account end dot on step 05 */
.how-step-final::after{
  content:"";position:absolute;top:-4px;right:12px;width:8px;height:8px;
  border-radius:50%;background:#2E6A4C;
  transition:transform var(--dur-mark) var(--ease),opacity var(--dur-mark) var(--ease);
  transition-delay:calc(var(--d,0ms) + 640ms);
}
.js .how-step-final:not(.on)::after{transform:scale(0);opacity:0}
/* Single-column (mobile): connector becomes a vertical progression
   down the left edge of the stack */
@media (max-width:479px){
  .how-step::before{
    top:16px;bottom:16px;left:-1px;right:auto;width:2px;height:auto;
    transform-origin:top center;
  }
  .js .how-step:not(.on)::before{transform:scaleY(0)}
  .how-step-final::after{top:auto;bottom:12px;right:auto;left:-4px}
}
@media (prefers-reduced-motion: reduce){
  .js .how-step::before{transition:none;transform:none}
  .js .how-step-final::after{transition:none;transform:none;opacity:1}
}

/* --- Focus states (WCAG AA visible focus) --- */
.nwcs-cta:focus-visible,
.nwcs-page a:focus-visible,
.nwcs-hdr a:focus-visible,
.nwcs-hdr summary:focus-visible{
  outline:2px solid #2E6A4C;
  outline-offset:3px;
  border-radius:6px;
}

/* --- Skip link --- */
.skip-link{
  position:absolute;left:-9999px;top:0;z-index:2000;
  background:#111;color:#fff;padding:12px 18px;border-radius:0 0 8px 0;
  font:700 14px Manrope,sans-serif;
}
.skip-link:focus{left:0}

/* ============================================================
   Header
   ============================================================ */
.nwcs-hdr a{text-decoration:none}
.nwcs-hdr details > summary{list-style:none;cursor:pointer}
.nwcs-hdr details > summary::-webkit-details-marker{display:none}
.nwcs-dd[open] .nwcs-dd-panel{display:block !important}
.nwcs-burger{display:none}
.nwcs-mpanel{display:none}
.nwcs-mob[open] .nwcs-mpanel{display:flex !important}
@media (max-width:880px){
  .nwcs-desk{display:none !important}
  .nwcs-burger{display:flex !important}
}

/* ------------------------------------------------------------
   Navigation motion (Batch M2). [open] toggles display, so the
   panels ease in via a keyframe (a transition can't run from
   display:none); closing stays instant. Menus remain fully
   keyboard-/<summary>-operable and work without JS.
   ------------------------------------------------------------ */
.nwcs-dd[open] .nwcs-dd-panel,
.nwcs-mob[open] .nwcs-mpanel{animation:nwcs-menu-in var(--hover-dur) var(--ease)}
@keyframes nwcs-menu-in{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:none}}

/* Chevron reflects open state (Emphasis pattern: rotate 180°) */
.nwcs-chev{display:inline-block;transition:transform var(--hover-dur) var(--ease)}
.nwcs-dd[open] .nwcs-chev{transform:rotate(180deg)}

/* Burger three-bar icon eases toward an X on [open] */
.nwcs-burger summary span{transition:transform var(--hover-dur) var(--ease),opacity var(--hover-dur) var(--ease)}
.nwcs-mob[open] summary span:nth-child(1){transform:translateY(6px) rotate(45deg)}
.nwcs-mob[open] summary span:nth-child(2){opacity:0}
.nwcs-mob[open] summary span:nth-child(3){transform:translateY(-6px) rotate(-45deg)}

/* Faint header elevation once scrolled (class toggled in site.js).
   Batch 3: the elevation is an affordance, not an animation — under reduced
   motion the class still toggles (site.js no longer early-returns) and the
   shadow snaps in with no transition (see the reduced-motion block below). */
.nwcs-hdr{transition:box-shadow var(--hover-dur) var(--ease)}
.nwcs-hdr-elev{box-shadow:0 10px 30px -22px rgba(17,17,17,.28)}

/* Dropdown / mobile menu hover (replaces runtime style-hover="background:#F6F6F3").
   Hover is capability-gated (Batch 1): a tap must not leave a stuck highlight. */
.menu-link{transition:background-color var(--hover-dur) var(--ease),transform var(--hover-dur) var(--ease)}
@media (hover: hover){
  .menu-link:hover{background:#F6F6F3}
  /* …release: spring back out of the press */
  .menu-link:hover:not(:active){transition:background-color var(--hover-dur) var(--ease),transform var(--hover-dur) var(--ease-spring)}
}
/* Press (Batch 1): the estate-wide .nwcs-cta grammar */
.menu-link:active{transform:translateY(1px);transition:background-color var(--hover-dur) var(--ease),transform var(--dur-press) var(--ease)}

@media (prefers-reduced-motion: reduce){
  .nwcs-dd[open] .nwcs-dd-panel,
  .nwcs-mob[open] .nwcs-mpanel{animation:none}
  /* state indicators keep their end state; only the motion is dropped.
     .nwcs-hdr: the elevation shadow still applies — instantly. Motion
     removed, information kept (Batch 3). */
  .nwcs-chev,.nwcs-burger summary span,.nwcs-hdr,.menu-link{transition:none}
  .nwcs-hdr-elev{box-shadow:0 10px 30px -22px rgba(17,17,17,.28)}
  .menu-link:active{transform:none}
}

/* ============================================================
   Footer
   ============================================================ */
.nwcs-ftr a{text-decoration:none}
/* Batch M3: footer links fade toward white and gain the underline
   reveal. !important wins over the inline link colours; the footer
   CTA button is excluded (it belongs to the CTA system).
   align-self keeps the hairline to the text width inside the
   stacked flex columns. */
.nwcs-ftr a:not(.nwcs-cta){position:relative;align-self:flex-start;transition:color var(--hover-dur) var(--ease),transform var(--hover-dur) var(--ease)}
.nwcs-ftr a:not(.nwcs-cta)::after{
  content:"";position:absolute;left:0;right:0;bottom:-3px;height:1.5px;
  background:currentColor;border-radius:2px;
  transform:scaleX(0);transform-origin:left center;
  transition:transform var(--hover-dur) var(--ease);
}
.nwcs-ftr a:not(.nwcs-cta):focus-visible::after{transform:scaleX(1)}
@media (hover: hover){
  .nwcs-ftr a:not(.nwcs-cta):hover{color:#fff !important}
  .nwcs-ftr a:not(.nwcs-cta):hover::after{transform:scaleX(1)}
  .nwcs-ftr a:not(.nwcs-cta):hover:not(:active){transition:color var(--hover-dur) var(--ease),transform var(--hover-dur) var(--ease-spring)}
}
/* Press (Batch 1) */
.nwcs-ftr a:not(.nwcs-cta):active{transform:translateY(1px);transition:color var(--hover-dur) var(--ease),transform var(--dur-press) var(--ease)}
@media (prefers-reduced-motion: reduce){
  .nwcs-ftr a:not(.nwcs-cta):active{transform:none}
}
.nwcs-ftr a:focus-visible{outline:2px solid #4f9e78;outline-offset:3px;border-radius:2px}

/* ============================================================
   Hover utilities (replace runtime style-hover="…")
   ============================================================ */
/* ------------------------------------------------------------
   Service-card micro-interaction system.
   One grammar across all five cards: on hover/focus the card lifts,
   the border tints, a green hairline draws along the top edge, and
   the step number + trailing arrow step forward together.
   transform / opacity / border-color only.
   ------------------------------------------------------------ */

/* White service / related cards: lift + softened green border.
   Batch 2 (declared evolution 1): the hover border tint steps back to the
   #7FB79C tint so the #2E6A4C hairline below is the lead voice, matching the
   hierarchy the dark .lift-hover card already had. */
.card-hover{position:relative;transition:border-color var(--hover-dur) var(--ease),transform var(--hover-dur) var(--ease)}
.card-hover:focus-visible{border-color:#7FB79C;transform:translateY(-3px)}
@media (hover: hover){
  .card-hover:hover{border-color:#7FB79C;transform:translateY(-3px)}
  .card-hover:hover:not(:active){transition:border-color var(--hover-dur) var(--ease),transform var(--hover-dur) var(--ease-spring)}
}
/* Press (Batch 1). The !important these rules carried in Group 1 is gone: the
   reveal observer no longer leaves an inline transform to beat (see .rv above). */
.card-hover:active{transform:translateY(1px);transition:border-color var(--hover-dur) var(--ease),transform var(--dur-press) var(--ease)}

/* Top hairline draw — same line grammar as the hero and the
   five-step connector */
.card-hover::before,
.lift-hover::before{
  content:"";position:absolute;top:-1px;left:16px;right:16px;height:2px;
  background:#2E6A4C;border-radius:2px;
  transform:scaleX(0);transform-origin:left center;
  transition:transform var(--hover-dur) var(--ease);
}
.lift-hover::before{background:#7FB79C}
.card-hover:focus-visible::before,
.lift-hover:focus-visible::before{transform:scaleX(1)}
@media (hover: hover){
  .card-hover:hover::before,
  .lift-hover:hover::before{transform:scaleX(1)}
}

/* Number and arrow move in concert */
.card-hover > div:first-child,
.card-hover > span,
.lift-hover > span{transition:transform var(--hover-dur) var(--ease)}
.card-hover:focus-visible > div:first-child,
.card-hover:focus-visible > span,
.lift-hover:focus-visible > span{transform:translateX(4px)}
@media (hover: hover){
  .card-hover:hover > div:first-child,
  .card-hover:hover > span,
  .lift-hover:hover > span{transform:translateX(4px)}
}

/* Dark specialism card: lift only */
.lift-hover{position:relative;transition:transform var(--hover-dur) var(--ease)}
.lift-hover:focus-visible{transform:translateY(-3px)}
@media (hover: hover){
  .lift-hover:hover{transform:translateY(-3px)}
  .lift-hover:hover:not(:active){transition:transform var(--hover-dur) var(--ease-spring)}
}
.lift-hover:active{transform:translateY(1px);transition:transform var(--dur-press) var(--ease)}

/* "Where this leads" related cards: green border only.
   Group 2 spec-integrity fix B: the border-color segment carried no timing
   function, so it fell back to the implicit `ease` the system bans. */
.border-hover{transition:border-color var(--hover-dur) var(--ease),transform var(--hover-dur) var(--ease)}
.border-hover:focus-visible{border-color:#2E6A4C}
@media (hover: hover){
  .border-hover:hover{border-color:#2E6A4C}
  .border-hover:hover:not(:active){transition:border-color var(--hover-dur) var(--ease),transform var(--hover-dur) var(--ease-spring)}
}
.border-hover:active{transform:translateY(1px);transition:border-color var(--hover-dur) var(--ease),transform var(--dur-press) var(--ease)}

/* Resting border for the white cards above. It used to sit in each card's
   inline style, where the shorthand's border-color beat the hover/focus
   tint (inline declaration vs author rule), so the tint never fired.
   Same values, moved into a class the (0,2,0) :hover/:focus-visible
   rules can win over. Station pages ship the same fix as .stn-onward. */
.card-rest{border:1px solid #E4E4E0}

/* ------------------------------------------------------------
   Link affordances (Batch M3). The underline reveal is the site's
   line-draw grammar — a hairline scaling from scaleX(0) on a left
   origin — applied as a link underline. currentColor keeps it
   legible on light and dark contexts alike.
   ------------------------------------------------------------ */
.link-uline{position:relative}
.link-uline::after{
  content:"";position:absolute;left:0;right:0;bottom:-3px;height:1.5px;
  background:currentColor;border-radius:2px;
  transform:scaleX(0);transform-origin:left center;
  transition:transform var(--hover-dur) var(--ease);
}
.link-uline:focus-visible::after{transform:scaleX(1)}
@media (hover: hover){
  .link-uline:hover::after{transform:scaleX(1)}
}

/* Desktop nav links: colour eases toward the accent + green
   underline reveal. !important wins over the inline active/idle
   colours set by build.mjs. */
.nwcs-nav-link{position:relative;transition:color var(--hover-dur) var(--ease),transform var(--hover-dur) var(--ease)}
.nwcs-nav-link::after{
  content:"";position:absolute;left:0;right:0;bottom:-5px;height:2px;
  background:#2E6A4C;border-radius:2px;
  transform:scaleX(0);transform-origin:left center;
  transition:transform var(--hover-dur) var(--ease);
}
.nwcs-nav-link:focus-visible::after{transform:scaleX(1)}
@media (hover: hover){
  .nwcs-nav-link:hover{color:#2E6A4C !important}
  .nwcs-nav-link:hover::after{transform:scaleX(1)}
  .nwcs-nav-link:hover:not(:active){transition:color var(--hover-dur) var(--ease),transform var(--hover-dur) var(--ease-spring)}
}
/* Press (Batch 1) */
.nwcs-nav-link:active{transform:translateY(1px);transition:color var(--hover-dur) var(--ease),transform var(--dur-press) var(--ease)}
/* Batch 3 — "you are here" speaks the underline: the current page has already
   drawn its line. Pre-drawn, and the transition is neutralised for this case so
   the line is never seen drawing itself in on page load (one vocabulary, two
   tenses). The inline active colour build.mjs sets is untouched. */
.nwcs-nav-link[aria-current]::after{transform:scaleX(1);transition:none}

/* ------------------------------------------------------------
   CTA system (Batch M1): one grammar for every call to action.
   Hover = restrained -1px lift (+ slight bg shift on solid
   variants). Press = quick .12s settle down; release springs
   back via the :hover rule's --ease-spring.
   transform / background-color only; focus-visible unchanged.
   ------------------------------------------------------------ */
.nwcs-cta{transition:transform var(--hover-dur) var(--ease),background-color var(--hover-dur) var(--ease)}
/* Press: settle down fast… */
.nwcs-cta:active{transform:translateY(1px);transition:transform var(--dur-press) var(--ease),background-color var(--hover-dur) var(--ease)}
/* Hover is capability-gated (Batch 1); the press above stays unconditional. */
@media (hover: hover){
  .nwcs-cta:hover{transform:translateY(-1px)}
  /* …release: spring back to the hover lift */
  .nwcs-cta:hover:not(:active){transition:transform var(--hover-dur) var(--ease-spring),background-color var(--hover-dur) var(--ease)}
  /* Solid-black CTAs (nav / footer-band / in-body primaries):
     slight bg shift on hover. !important is required to win over
     the per-button inline background styles. */
  .nwcs-cta-solid:hover{background:#1c1c1c !important}
  /* Inverted (white-on-dark) CTAs: footer band, dark-hero primaries */
  .nwcs-cta-inv:hover{background:#f2f2ee !important}
}

/* Reduced motion: keep colour affordances, drop movement (Bucket A #2).
   The hairline still appears — instantly — so the affordance survives. */
@media (prefers-reduced-motion: reduce){
  .card-hover,.lift-hover,.border-hover,.nwcs-cta,
  .card-hover::before,.lift-hover::before,
  .card-hover > div:first-child,.card-hover > span,.lift-hover > span{
    transition:border-color var(--hover-dur) var(--ease);
  }
  .card-hover:hover,.card-hover:focus-visible,
  .lift-hover:hover,.lift-hover:focus-visible,
  .nwcs-cta:hover,.nwcs-cta:active{transform:none}
  /* Batch 1 press: the movement goes, the :active state survives.
     (Group 2: !important dropped here too — nothing writes an inline
     transform onto these elements any more.) */
  .card-hover:active,.lift-hover:active,.border-hover:active{transform:none}
  .nwcs-nav-link:active{transform:none}
  /* colour affordance (solid/inverted bg shift) intentionally kept */
  .card-hover:hover > div:first-child,.card-hover:focus-visible > div:first-child,
  .card-hover:hover > span,.card-hover:focus-visible > span,
  .lift-hover:hover > span,.lift-hover:focus-visible > span{transform:none}
  /* Batch M3: the underline still appears — instantly — so the
     affordance survives without motion */
  .link-uline::after,.nwcs-nav-link::after,
  .nwcs-ftr a:not(.nwcs-cta)::after{transition:none}
  .nwcs-nav-link,.nwcs-ftr a:not(.nwcs-cta){transition:none}
}

/* ============================================================
   Proof engineering drawings (Milestone B1) — inline cross-sections
   drawn in the hero's line-draw grammar. Each card's strokes draw in
   when the card reveals (the existing .rv IntersectionObserver adds
   .on): strata fade first (--i:0), structure second (--i:1), the green
   accent + dimension ticks last (--i:2). Composed end-state is the
   default — no-JS and reduced-motion render every drawing fully drawn.
   The inline SVG's viewBox + aspect-ratio reserve a fixed 3:2 box from
   first paint, so there is zero layout shift. Reuses the hero keyframes
   (hero-draw / hero-fade) so the whole site shares one motion.
   ============================================================ */
.proof-art{display:block;width:100%;height:auto;aspect-ratio:3/2;background:#F6F6F3;border-bottom:1px solid #E4E4E0}
/* Pre-draw state: gated by the .js class (set in <head>) so no-JS keeps
   the composed drawing; neutralised below under reduced motion. */
.js .proof-art .pd{stroke-dasharray:1;stroke-dashoffset:1}
.js .proof-art .pf{opacity:0}
.js .rv.on .proof-art .pd{
  animation:hero-draw var(--draw-dur) var(--ease) forwards;
  animation-delay:calc(var(--d,0ms) + var(--i,0)*var(--stagger));
}
.js .rv.on .proof-art .pf{
  animation:hero-fade .8s var(--ease) forwards;
  animation-delay:calc(var(--d,0ms) + var(--i,0)*var(--stagger));
}
@media (prefers-reduced-motion: reduce){
  /* !important so the reduced-motion reset beats the higher-specificity
     .rv.on draw trigger (mirrors the site's .rv guard idiom). */
  .js .proof-art .pd{animation:none !important;stroke-dashoffset:0 !important}
  .js .proof-art .pf{animation:none !important;opacity:var(--o,1) !important}
}

/* ============================================================
   About hero — quiet strata / registration decoration (Milestone B3).
   Static (no parallax, no draw-in); sits behind the hero content and
   renders identically with no JS / reduced motion.
   ============================================================ */
/* Confined to the hero band's bottom padding zone — always below the intro
   paragraph at every width, so the register never sits behind text. Height
   stays under the section's padding-bottom (clamp(40px,5vw,60px)). */
.about-hero-art{position:absolute;left:0;right:0;bottom:0;height:clamp(34px,4.4vw,54px);pointer-events:none;overflow:hidden}
.about-hero-art svg{width:100%;height:100%;display:block}
.about-hero-art svg *{vector-effect:non-scaling-stroke}

/* Home hero trust row — the interpunct separators are single-line
   punctuation. The row is one line at >=920px; below that it wraps and the
   separators would dangle at line ends, so hide them and let the phrases
   wrap as tidy centered rows (the flex gap keeps them spaced). The desktop
   single-line row is unchanged. */
@media (max-width:919px){
  .trust-sep{display:none}
}

/* ============================================================
   Service dark-band chips — "measure-in" (Milestone B4).
   On scroll into view the band is an .rv, so the existing observer adds
   .on; each chip's 2px underline then draws scaleX 0→1 from the left,
   staggered L→R by --stagger, and the text brightens as its tick lands.
   Settled end state is quiet (underline ~45% opacity). Composed end-state
   is the default — no-JS and reduced-motion render every chip settled with
   no animation. Uses var(--dur-mark) (NOT --draw-dur, reserved for signature draws).
   ============================================================ */
.chip{position:relative;color:#f4f4ef;transition:color var(--dur-mark) var(--ease)}
.chip::after{
  content:"";position:absolute;left:16px;right:16px;bottom:7px;height:2px;
  background:#7FB79C;border-radius:2px;opacity:.45;
  transform:scaleX(1);transform-origin:left center;
  transition:transform var(--dur-mark) var(--ease),opacity var(--dur-mark) var(--ease);
}
.chip-band .chip:nth-child(1){--ci:0}
.chip-band .chip:nth-child(2){--ci:1}
.chip-band .chip:nth-child(3){--ci:2}
.chip-band .chip:nth-child(4){--ci:3}
.chip-band .chip:nth-child(5){--ci:4}
.chip-band .chip:nth-child(6){--ci:5}
.chip-band .chip:nth-child(7){--ci:6}
/* Pre-measure state under JS, before the band reveals */
.js .chip-band:not(.on) .chip{color:#cfcfca}
.js .chip-band:not(.on) .chip::after{transform:scaleX(0);opacity:0}
/* Staggered settle once revealed (delay on both the underline and the text) */
.js .chip-band.on .chip,
.js .chip-band.on .chip::after{transition-delay:calc(var(--ci,0)*var(--stagger))}
@media (prefers-reduced-motion: reduce){
  .chip,.chip::after{transition:none}
  .js .chip-band:not(.on) .chip{color:#f4f4ef}
  .js .chip-band:not(.on) .chip::after{transform:scaleX(1);opacity:.45}
}

/* Contact brief-builder (composer, B2) — animated rules; layout inline; .bb-* */
.bb{display:none}
.js .bb{display:block}
.js .bb-static{display:none}
.bb-chip{appearance:none;font:500 13.5px Manrope,sans-serif;cursor:pointer;border:1px solid #E4E4E0;background:#F6F6F3;color:#5C5C56;border-radius:999px;padding:8px 15px;display:inline-flex;align-items:center;gap:7px;transition:background-color var(--hover-dur) var(--ease),border-color var(--hover-dur) var(--ease),color var(--hover-dur) var(--ease),transform var(--hover-dur) var(--ease-spring)}
@media (hover: hover){
  .bb-chip:hover{border-color:#7FB79C}
}
.bb-chip:active{transform:translateY(1px) scale(.985);transition:transform var(--dur-press) var(--ease)}
/* Batch 4: the two pick-one groups are real radios, so the selected state is
   carried by aria-checked there and by aria-pressed on the multi-select
   groups. One selected look, two correct semantics. */
.bb-chip[aria-pressed="true"],
.bb-chip[aria-checked="true"]{background:#E7F0EB;border-color:#7FB79C;color:#2E6A4C;font-weight:600}
.bb-chip:focus-visible{outline:2px solid #2E6A4C;outline-offset:2px}
.bb-tick{width:14px;height:14px;flex:0 0 14px;opacity:0;transform:scale(.6);transition:opacity var(--hover-dur) var(--ease),transform var(--hover-dur) var(--ease-spring)}
.bb-chip[aria-pressed="true"] .bb-tick,
.bb-chip[aria-checked="true"] .bb-tick{opacity:1;transform:scale(1)}
.bb-date:focus-visible{outline:2px solid #2E6A4C;outline-offset:2px;border-color:#7FB79C}
/* Batch 4 — the date field acknowledges being superseded. When a quick-pick
   chip clears a typed date the field dips to muted and back: opacity only (the
   colour and border are inline on the input), no transform, no layout. Under
   reduced motion site.js never adds the class — the instant clear is the
   information. */
.bb-date{transition:opacity var(--dur-mark) var(--ease)}
.bb-date-dip{opacity:.4}
.bb-slot{position:relative;display:inline-grid;vertical-align:baseline;max-width:100%;color:#111;transition:width .25s var(--ease)}
.bb-slot .bb-g,.bb-slot .bb-k{grid-area:1/1;min-width:0;transition:opacity .26s var(--ease),transform .34s var(--ease)}
.bb-slot .bb-g{white-space:nowrap;color:#5C5C56;font-weight:500;opacity:.35;border-bottom:1px dashed #7FB79C}
.bb-slot .bb-k{color:#2E6A4C;font-weight:600;opacity:0;transform:translateY(6px);pointer-events:none}
.bb-slot.inked .bb-g{opacity:0;transform:translateY(-6px)}
.bb-slot.inked .bb-k{opacity:1;transform:translateY(0)}
.bb-subject .bb-slot .bb-g{border-bottom-color:#cfcfca}
.bb-slot::after{content:"";position:absolute;left:0;right:0;bottom:-1px;height:2px;background:#2E6A4C;border-radius:2px;opacity:0;transform:scaleX(0);transform-origin:left center;transition:transform .30s var(--ease),opacity .30s var(--ease)}
.bb-slot.sweep::after{transform:scaleX(1);opacity:1}
.bb-slot.settled::after{height:1px;opacity:.2;background:#7FB79C;transition:transform .30s var(--ease),opacity .6s var(--ease),height .6s var(--ease),background-color .6s var(--ease)}
.bb-dot{background:#7FB79C;transition:background-color var(--hover-dur) var(--ease)}
.bb-progress.done .bb-dot{background:#2E6A4C}
.bb-cta{display:inline-flex;align-items:center;gap:8px;font:700 15px Manrope,sans-serif;color:#111;background:transparent;border:1.5px solid #111;padding:12px 22px;border-radius:9px;transition:background-color var(--hover-dur) var(--ease),color var(--hover-dur) var(--ease),transform var(--hover-dur) var(--ease)}
.bb-cta.full{background:#111;color:#fff}
@media (hover: hover){
  .bb-cta.full:hover{background:#1c1c1c !important}
}
@media (prefers-reduced-motion: reduce){
  .bb-chip,.bb-tick,.bb-dot,.bb-cta,.bb-slot,.bb-slot .bb-g,.bb-slot .bb-k,.bb-slot::after,
  .bb-date{transition:none}
}

/* ===========================================================================
   Lane A snag pass — 2026-08-07 (overall design review nwcs-overall-2026-08-06)
   Defect fixes only. No new design language; see run-packets/ for the routed
   design work that owns these surfaces.
   =========================================================================== */

/* A-16 — hero CTA pairs differed by 2px in height because the secondary carries
   a 1px border inline and the solid primary carries none. Give every CTA a
   border box by default; the inline `border:1px solid #D6D6D0` on secondaries
   still wins, so only the borderless primaries change. */
.nwcs-cta{border:1px solid transparent}

/* A-15 — the mobile menu is a 300px panel over live, scrollable content.
   Lock the page behind it while it is open (JS toggles this class).
   A scrim is deliberately NOT added here: that is packet 04's territory. */
.nwcs-menu-open{overflow:hidden}

/* A-07 — the composer's send action was fully live at `0 of 4`, so a visitor
   could fire an empty enquiry. JS sets aria-disabled until at least one answer
   is given; this is the matching affordance. */
.bb-cta[aria-disabled="true"]{opacity:.5;cursor:not-allowed}
.bb-cta[aria-disabled="true"]:hover{transform:none}
@media (prefers-reduced-motion: reduce){
  .bb-cta[aria-disabled="true"]:hover{transform:none}
}

/* ===========================================================================
   STATION PAGES (Milestone 1, commission 2026-08-19) — "one line, four
   stations". Each service page is a station on the same engagement line the
   home page's five-step connector draws. Estimating is station 02 (Measure).

   Every rule below is namespaced .stn-* and only bites on markup that opts
   in, so no other route changes. Reuses the shipped drawing idioms verbatim:
   pathLength="1" + stroke-dasharray:1/stroke-dashoffset:1, the hero-draw /
   hero-fade keyframes, .pd/.pf pre-draw states gated on .js, the --i stagger,
   the --d batch delay written by site.js, and the !important reduced-motion
   neutralisation that has to beat the higher-specificity .on triggers.
   =========================================================================== */

/* --- The station drawing --------------------------------------------------
   viewBox + aspect-ratio reserve the box from first paint (zero CLS), exactly
   like .proof-art. --dd lets the ghost and structure layers run shorter than
   the signature --draw-dur; the measurement layer keeps the full --draw-dur so
   the green always lands last — that landing is the moment. Shared by all four
   station pages. */
.stn-art{display:block;width:100%;height:auto;aspect-ratio:5/4}
.js .stn-art .pd{stroke-dasharray:1;stroke-dashoffset:1}
.js .stn-art .pf{opacity:0}
.js .rv.on .stn-art .pd{
  animation:hero-draw var(--dd,var(--draw-dur)) var(--ease) forwards;
  animation-delay:calc(var(--d,0ms) + var(--i,0)*var(--stagger));
}
.js .rv.on .stn-art .pf{
  animation:hero-fade .8s var(--ease) forwards;
  animation-delay:calc(var(--d,0ms) + var(--i,0)*var(--stagger));
}
@media (prefers-reduced-motion: reduce){
  .js .stn-art .pd{animation:none !important;stroke-dashoffset:0 !important}
  .js .stn-art .pf{animation:none !important;opacity:var(--o,1) !important}
}

/* --- Station hero layout --------------------------------------------------
   Text block left, station drawing right (~42%) at >=980px; below that the
   drawing stacks full-width beneath the text block. */
.stn-hero{display:grid;gap:clamp(28px,4vw,44px);align-items:center}
@media (min-width:980px){
  .stn-hero{grid-template-columns:minmax(0,1fr) minmax(0,.72fr)}
}
/* Narrow screens: the primary and secondary hero CTA read as a balanced
   pair rather than two differently-sized blocks. */
@media (max-width:480px){
  .stn-cta-row > .nwcs-cta{flex:1 1 auto;text-align:center}
}

/* --- Station marker (01-05 on one drawn line) ------------------------------
   The line draws scaleX 0->1 from the left after the hero drawing settles:
   the measurement layer starts at 11 stagger steps and runs --draw-dur, so
   the marker follows at (that end) + 200ms. Composed by default; the pre-draw
   state is gated on .js, so no-JS renders the finished line. */
.stn-marker{position:relative;width:100%;max-width:330px;margin-top:clamp(24px,3vw,32px)}
.stn-marker-rail{position:absolute;left:10%;right:10%;top:13px;height:2px;pointer-events:none}
.stn-marker-rail span{
  position:absolute;top:0;height:2px;border-radius:2px;
  transform-origin:left center;
  transition:transform .6s var(--ease);
  transition-delay:calc(var(--d,0ms) + 11*var(--stagger) + var(--draw-dur) + 200ms);
}
.stn-rail-rest{left:0;right:0;background:#D6D6D0}
.stn-rail-live{left:0;width:25%;background:#2E6A4C}
.js .stn-marker:not(.on) .stn-marker-rail span{transform:scaleX(0)}
.stn-marker-list{display:grid;grid-template-columns:repeat(5,1fr);list-style:none;margin:0;padding:0}
.stn-marker-list > li{display:flex;justify-content:center;min-width:0}
/* 44px minimum hit area, padded around an 8px tick */
.stn-stop{
  display:flex;flex-direction:column;align-items:center;gap:7px;
  min-width:44px;min-height:44px;padding:10px 4px 8px;border-radius:8px;
  text-decoration:none;position:relative;
  transition:background-color var(--hover-dur) var(--ease),transform var(--hover-dur) var(--ease);
}
.stn-tick{width:8px;height:8px;border-radius:1px;background:#D6D6D0;flex:0 0 auto;transition:background-color var(--hover-dur) var(--ease)}
.stn-num{font:600 12px Manrope,sans-serif;letter-spacing:.06em;color:#6E6E68;transition:color var(--hover-dur) var(--ease)}
.stn-stop-done .stn-tick{background:#2E6A4C}
/* Current station: filled green square, slightly larger. Negative margins keep
   the 8px layout box (and therefore the rail alignment) unchanged. */
.stn-stop-current .stn-tick{width:11px;height:11px;margin:-1.5px 0;background:#2E6A4C}
.stn-stop-current .stn-num{font-weight:700;color:#2E6A4C}
/* Batch 2: the pill background steps to the #E7F0EB tint — #F6F6F3 was a ~1%
   luminance step against the page and read as no hover state at all. */
a.stn-stop:focus-visible{background:#E7F0EB}
a.stn-stop:focus-visible .stn-tick{background:#7FB79C}
a.stn-stop:focus-visible .stn-num{color:#2E6A4C}
@media (hover: hover){
  a.stn-stop:hover{background:#E7F0EB}
  a.stn-stop:hover .stn-tick{background:#7FB79C}
  a.stn-stop:hover .stn-num{color:#2E6A4C}
  a.stn-stop:hover:not(:active){transition:background-color var(--hover-dur) var(--ease),transform var(--hover-dur) var(--ease-spring)}
}
/* Press (Batch 1) */
a.stn-stop:active{transform:translateY(1px);transition:background-color var(--hover-dur) var(--ease),transform var(--dur-press) var(--ease)}
/* Visually-hidden destination names — "03" is not a link label. */
.stn-sr{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0}
@media (prefers-reduced-motion: reduce){
  .stn-marker-rail span{transition:none !important}
  .js .stn-marker:not(.on) .stn-marker-rail span{transform:none !important}
  .stn-stop,.stn-tick,.stn-num{transition:none}
  a.stn-stop:active{transform:none}
}

/* --- Eyebrow underline (the station micro-signature) -----------------------
   24px x 2px accent rule, scaleX 0->1 from the left when its .rv reveals.
   Composed by default; the pre-draw state is gated on .js. */
.stn-eyebrow{position:relative;display:inline-block;padding-bottom:10px}
.stn-eyebrow::after{
  content:"";position:absolute;left:0;bottom:0;width:24px;height:2px;
  background:#2E6A4C;border-radius:2px;
  transform:scaleX(1);transform-origin:left center;
  transition:transform var(--dur-mark) var(--ease);
  transition-delay:calc(var(--d,0ms) + 140ms);
}
.stn-eyebrow-inv::after{background:#7FB79C}
.js .rv:not(.on) .stn-eyebrow::after{transform:scaleX(0)}
@media (prefers-reduced-motion: reduce){
  .stn-eyebrow::after{transition:none !important}
  .js .rv:not(.on) .stn-eyebrow::after{transform:scaleX(1) !important}
}

/* --- Card rail (the anatomy of a take-off) ---------------------------------
   A drawn hairline above the four-card grid with one node over each column.
   Only shown where the auto-fit grid is actually four columns wide
   (4*250 + 3*16 gutters + 2*32 page padding = 1112px); below that the cards
   stack and the rail would be a lie, so it is not rendered. */
.stn-cardrail{display:none}
@media (min-width:1120px){
  .stn-cardrail{display:block;position:relative;height:2px;margin:0 0 26px}
}
.stn-cardrail-line{
  position:absolute;left:0;right:0;top:0;height:2px;background:#D6D6D0;border-radius:2px;
  transform-origin:left center;
  transition:transform .9s var(--ease);
  transition-delay:var(--d,0ms);
}
.stn-cardrail-node{
  position:absolute;top:-3px;width:8px;height:8px;margin-left:-4px;border-radius:1px;
  background:#D6D6D0;opacity:1;
  transition:transform var(--dur-mark) var(--ease),background-color var(--dur-mark) var(--ease),opacity var(--dur-mark) var(--ease);
  transition-delay:calc(var(--d,0ms) + 300ms + var(--ni,0)*var(--stagger));
}
/* Column centres for a four-up grid with 16px gutters */
.stn-n1{left:calc((100% - 48px)/8);--ni:0}
.stn-n2{left:calc((100% - 48px)*3/8 + 16px);--ni:1}
.stn-n3{left:calc((100% - 48px)*5/8 + 32px);--ni:2}
.stn-n4{left:calc((100% - 48px)*7/8 + 48px);--ni:3}
.stn-cardrail.on .stn-cardrail-node{background:#2E6A4C}
.js .stn-cardrail:not(.on) .stn-cardrail-line{transform:scaleX(0)}
.js .stn-cardrail:not(.on) .stn-cardrail-node{transform:scale(0);opacity:0}
@media (prefers-reduced-motion: reduce){
  .stn-cardrail-line,.stn-cardrail-node{transition:none !important}
  .js .stn-cardrail:not(.on) .stn-cardrail-line{transform:none !important}
  .js .stn-cardrail:not(.on) .stn-cardrail-node{transform:none !important;opacity:1 !important}
}

/* --- Card resting grammar --------------------------------------------------
   These four cards are non-interactive <div>s, so they take the resting half
   of the shipped .card-hover grammar — the same 2px accent hairline on the top
   edge, identical geometry and tokens — drawn on reveal instead of on hover.
   The lift and the border tint stay reserved for the link cards. */
.stn-card{position:relative}
.stn-card::before{
  content:"";position:absolute;top:-1px;left:16px;right:16px;height:2px;
  background:#2E6A4C;border-radius:2px;
  transform:scaleX(1);transform-origin:left center;
  transition:transform var(--hover-dur) var(--ease);
  transition-delay:calc(var(--d,0ms) + 180ms);
}
.js .stn-card:not(.on)::before{transform:scaleX(0)}
@media (prefers-reduced-motion: reduce){
  .stn-card::before{transition:none !important}
  .js .stn-card:not(.on)::before{transform:scaleX(1) !important}
}

/* --- Onward-card arrow step (the shipped .card-hover > span idiom, applied
   to the arrow glyph inside the heading of a .border-hover card).
   Batch 2: aliased to .arw so any onward card can take the step without
   inheriting the stn- namespace (groundworks uses .arw). ------------------ */
.arw,.stn-arw{display:inline-block;transition:transform var(--hover-dur) var(--ease)}
.border-hover:focus-visible .arw,
.border-hover:focus-visible .stn-arw{transform:translateX(4px)}
@media (hover: hover){
  .border-hover:hover .arw,
  .border-hover:hover .stn-arw{transform:translateX(4px)}
}
@media (prefers-reduced-motion: reduce){
  .arw,.stn-arw{transition:none}
  .border-hover:hover .arw,.border-hover:hover .stn-arw,
  .border-hover:focus-visible .arw,
  .border-hover:focus-visible .stn-arw{transform:none}
}

/* ===========================================================================
   STATION PAGES (Milestone 2, commission 2026-08-19) — stations 03 (Lodge)
   and 04 (Agree), plus the Estimating art-direction revision. Append-only and
   namespaced .stn-*, exactly as Milestone 1.
   =========================================================================== */

/* --- Hero stack order -----------------------------------------------------
   The hero is now three grid children (copy / drawing / marker) so the mobile
   stack can read CTAs -> drawing -> station marker: the marker is the
   drawing's scale bar, so it belongs under the drawing, not above it.
   At >=980px the copy and the marker sit back in column 1, rows 1 and 2, and
   the drawing spans both rows in column 2 — align-self end/start keeps the
   copy and the marker adjacent when the drawing is the taller column. */
.stn-hero{row-gap:clamp(28px,4vw,44px);column-gap:clamp(28px,4vw,44px)}
.stn-hero > .stn-marker{margin-top:0}
@media (min-width:980px){
  .stn-hero{row-gap:clamp(24px,3vw,32px);grid-template-rows:auto auto}
  .stn-hero > .stn-hero-copy{grid-column:1;grid-row:1;align-self:end}
  .stn-hero > .stn-hero-art{grid-column:2;grid-row:1 / span 2;align-self:center}
  .stn-hero > .stn-marker{grid-column:1;grid-row:2;align-self:start}
}

/* --- Portrait station drawing ---------------------------------------------
   Reserved for a station whose subject is a portrait document. Unused at the
   moment (station 03 stayed 5/4) but kept so the ratio is a token, not a
   magic number, if a later station needs it. */
.stn-art-portrait{aspect-ratio:4/5}

/* --- Onward-card border ----------------------------------------------------
   The estate ships `.border-hover:hover{border-color:#2E6A4C}` but the onward
   cards carried `border:1px solid #E4E4E0` inline, so the hover tint could
   never win. Same resting values, moved into a class, so the shipped hover and
   focus-visible tint actually fires. .border-hover:hover is (0,2,0) and beats
   this (0,1,0). */
.stn-onward{border:1px solid #E4E4E0}

/* --- Document spine (station 03 — "What a return includes") ----------------
   The section is the return's table of contents: a drawn vertical rule with a
   square section marker per card, each card hung off it by a short horizontal
   tick. The rule draws top->bottom (scaleY, top origin) — the vertical
   counterpart of the estate's scaleX left-origin rule — then the markers tick
   in on each card's own stagger and turn green in order. No terminal dot. */
.stn-spine{position:relative;max-width:720px;padding-left:38px;display:flex;flex-direction:column;gap:16px}
.stn-spine-rail{position:absolute;left:0;top:0;bottom:0;width:2px;pointer-events:none}
.stn-spine-rule{
  position:absolute;left:0;top:0;width:2px;height:100%;
  background:#D6D6D0;border-radius:2px;
  transform-origin:top center;
  transition:transform .9s var(--ease);
  transition-delay:var(--d,0ms);
}
.js .stn-spine-rail:not(.on) .stn-spine-rule{transform:scaleY(0)}
/* Section marker on the spine + the tick that connects the card to it.
   .stn-card::before is the top hairline, so the connector takes ::after. */
.stn-spine-card{margin-left:0}
.stn-spine-mark{
  position:absolute;left:-42px;top:31px;width:10px;height:10px;border-radius:1px;
  background:#D6D6D0;
  transition:transform var(--dur-mark) var(--ease),background-color var(--dur-mark) var(--ease),opacity var(--dur-mark) var(--ease);
  transition-delay:calc(var(--d,0ms) + 300ms);
}
.stn-spine-card::after{
  content:"";position:absolute;left:-32px;top:35px;width:32px;height:2px;
  background:#D6D6D0;border-radius:2px;
  transform-origin:left center;
  transition:transform var(--dur-mark) var(--ease);
  transition-delay:calc(var(--d,0ms) + 220ms);
}
.stn-spine-card.on .stn-spine-mark{background:#2E6A4C}
.js .stn-spine-card:not(.on) .stn-spine-mark{transform:scale(0);opacity:0}
.js .stn-spine-card:not(.on)::after{transform:scaleX(0)}
@media (max-width:559px){
  .stn-spine{padding-left:30px}
  .stn-spine-mark{left:-34px}
  .stn-spine-card::after{left:-24px;width:24px}
}
@media (prefers-reduced-motion: reduce){
  .stn-spine-rule,.stn-spine-mark,.stn-spine-card::after{transition:none !important}
  .js .stn-spine-rail:not(.on) .stn-spine-rule{transform:none !important}
  .js .stn-spine-card:not(.on) .stn-spine-mark{transform:none !important;opacity:1 !important}
  .js .stn-spine-card:not(.on)::after{transform:none !important}
}

/* --- The held line (station 04 — "What we hold the line on") ---------------
   One level hairline across the full content width, drawn scaleX 0->1 from the
   left. Nothing bends or deflects it; that is the meaning. The four cards hang
   below it, each joined by a short vertical drop-tick that draws scaleY from
   the top on the card's own stagger. Where the grid stops being four-up the
   cards no longer sit under the line, so the ticks are dropped and only the
   line survives. No terminal dot. */
.stn-held{position:relative;height:2px;margin:0 0 30px}
.stn-held-line{
  position:absolute;left:0;right:0;top:0;height:2px;background:#D6D6D0;border-radius:2px;
  transform-origin:left center;
  transition:transform .9s var(--ease);
  transition-delay:var(--d,0ms);
}
.js .stn-held:not(.on) .stn-held-line{transform:scaleX(0)}
.stn-held-card::after{
  content:"";position:absolute;top:-24px;left:50%;margin-left:-1px;width:2px;height:18px;
  background:#2E6A4C;border-radius:2px;
  transform-origin:top center;
  transition:transform var(--dur-mark) var(--ease);
  transition-delay:calc(var(--d,0ms) + 260ms);
}
.js .stn-held-card:not(.on)::after{transform:scaleY(0)}
@media (max-width:1119px){
  .stn-held-card::after{content:none}
}
@media (prefers-reduced-motion: reduce){
  .stn-held-line,.stn-held-card::after{transition:none !important}
  .js .stn-held:not(.on) .stn-held-line{transform:none !important}
  .js .stn-held-card:not(.on)::after{transform:none !important}
}

/* --- Chip-band wrapping control -------------------------------------------
   At 1280-1600 the five-chip bands wrapped 4+1 and left a single orphan chip
   on its own row. The constraint goes on the BAND, never on .chip: capping the
   band's measure forces an even 3+2 wrap across that whole range. */
.stn-chipband{max-width:840px}

/* ===========================================================================
   STATION PAGES (Milestone 3, commission 2026-08-19) — station 05 (Close out),
   the last station on the line. Append-only and namespaced .stn-*, exactly as
   Milestones 1 and 2. Two rules, both about one idea: a line that ends.
   =========================================================================== */

/* --- The close-out dot (station 05 only) -----------------------------------
   Every other station stop is a square tick, because the line runs on through
   it. Station 05 is where it arrives, so its stop is a filled dot. Geometry is
   untouched: .stn-stop-current already sets 11px and the -1.5px margins that
   keep the 8px layout box, so the rail alignment and the 44px hit area are
   identical to the other three pages — only the corner radius changes. The
   circular dot is reserved for close-out and appears nowhere else in the set. */
.stn-stop-dot .stn-tick{border-radius:50%}

/* --- Terminating card rail (station 05 — "What we keep on top of") ---------
   Estimating's rail runs the full content width because a take-off has no end.
   This one is the contract timeline: it stops at the fourth node — Final
   account — and nothing continues past it. Same machinery and the same node
   positions; the line is width-capped to node 4's centre instead of stretched
   edge to edge, and node 4 takes the close-out dot. Both selectors are (0,2,0)
   so they beat the base rules without !important, and the shipped
   reduced-motion block still matches on the unchanged class names. */
.stn-cardrail-end .stn-cardrail-line{right:auto;width:calc((100% - 48px)*7/8 + 48px)}
.stn-cardrail-end .stn-n4{width:11px;height:11px;margin-left:-5.5px;top:-4.5px;border-radius:50%}

/* ===========================================================================
   Groundworks sector page (.gw-*) — commission 2026-08-19, M1 — "the dark
   sheet". The estate draws light ink on light paper everywhere else; this is
   its only dark band, so this page is the drawing sheet itself: dark paper,
   light ink. Every rule is namespaced .gw-* and appended, so nothing shared
   and nothing .stn-* moves. The drawing machinery is the shipped station
   idiom replicated under .gw- names — pathLength="1" + stroke-dasharray:1 /
   stroke-dashoffset:1, the hero-draw / hero-fade keyframes, .pd/.pf pre-draw
   states gated on .js, the --i stagger and the --d batch delay site.js writes,
   and the !important reduced-motion neutralisation that has to beat the
   higher-specificity .on triggers. Composed end-state is the default: no-JS
   and reduced motion render the sheet finished.
   =========================================================================== */

/* --- Hero layout ----------------------------------------------------------
   Copy left, the section drawing right at 42% from 980px up — the station
   hero geometry, widened because this drawing is the signature one. Below
   that it is a single column and the drawing becomes a wide crop that sits
   under the CTA row. */
.gw-hero{display:grid;gap:clamp(28px,4vw,44px);align-items:center}
@media (min-width:980px){
  .gw-hero{grid-template-columns:minmax(0,1fr) minmax(0,42%);gap:48px}
}

/* --- The section drawing --------------------------------------------------
   viewBox + aspect-ratio reserve both boxes from first paint, so the drawing
   costs zero CLS. --dd lets the ghost, ground, foundation and frame layers run
   shorter than the signature; the measurement layer keeps the full --draw-dur,
   so the green is still drawing when everything else has settled. --fd does
   the same job for the one faded layer (the ghost grid). Square caps are the
   drawn hand: a ruled line stops on its mark, it does not taper. */
.gw-art{display:block;width:100%;height:auto;aspect-ratio:1/1;stroke-linecap:square}
.gw-art-m{aspect-ratio:65/47}
.js .gw-art .pd{stroke-dasharray:1;stroke-dashoffset:1}
.js .gw-art .pf{opacity:0}
.js .rv.on .gw-art .pd{
  animation:hero-draw var(--dd,var(--draw-dur)) var(--ease) forwards;
  animation-delay:calc(var(--d,0ms) + var(--i,0)*var(--stagger));
}
.js .rv.on .gw-art .pf{
  animation:hero-fade var(--fd,.8s) var(--ease) forwards;
  animation-delay:calc(var(--d,0ms) + var(--i,0)*var(--stagger));
}
@media (prefers-reduced-motion: reduce){
  .js .gw-art .pd{animation:none !important;stroke-dashoffset:0 !important}
  .js .gw-art .pf{animation:none !important;opacity:var(--o,1) !important}
}

/* Exactly one instance is ever displayed. display:none keeps the other out of
   the layout and out of the IntersectionObserver, so it never gets .on and
   never animates — the visible instance is always the one that draws. */
.gw-hero-art{display:none}
.gw-art-m-box{display:block;margin-top:clamp(26px,4vw,34px)}
@media (min-width:980px){
  .gw-hero-art{display:block;width:100%;max-width:520px;justify-self:end}
  .gw-art-m-box{display:none}
}

/* --- Hero CTA pair --------------------------------------------------------
   The page's forward motion is the composer; the mailto stays, demoted to an
   outline-on-dark twin. Padding, radius and type match .nwcs-cta exactly (the
   element carries .nwcs-cta too, so the shipped press tokens and the hover
   lift apply unchanged) — only the surface differs. */
.gw-cta-row{display:flex;flex-wrap:wrap;gap:12px;margin-top:28px}
.gw-cta-ghost{
  font:700 15px Manrope,sans-serif;color:#f4f4ef;background:transparent;
  border:1px solid #3a3a38;padding:14px 24px;border-radius:9px;
  transition:transform var(--hover-dur) var(--ease),border-color var(--hover-dur) var(--ease);
}
.gw-cta-ghost:hover:not(:active){border-color:#7FB79C}
.gw-cta-ghost:active{border-color:#7FB79C}
/* On #111 the estate's #2E6A4C focus ring is nearly invisible; the footer
   already sets the precedent of a lighter ring on dark. Both hero CTAs take
   the accent's light tone, at the shipped 2px / 3px offset. Scoped to this
   row, so no shared focus rule changes. */
.gw-cta-row .nwcs-cta:focus-visible{outline:2px solid #7FB79C;outline-offset:3px;border-radius:9px}
/* Touch has no hover, so the press carries the state instead of leaving a
   dead rule behind. */
@media (hover:none){
  .gw-cta-ghost:hover{border-color:#3a3a38}
  .gw-cta-ghost:active{border-color:#7FB79C}
}
@media (max-width:480px){
  .gw-cta-row > .nwcs-cta{flex:1 1 auto;text-align:center}
}
@media (prefers-reduced-motion: reduce){
  .gw-cta-ghost{transition:border-color var(--hover-dur) var(--ease)}
}

/* --- The datum edge -------------------------------------------------------
   Full-bleed across the dark band, flush to its bottom: a ground line with
   survey ticks marching along it and one green level mark sitting on the
   content column's left margin. Fixed 44px, so it reserves its own space and
   costs no CLS. The band itself is composed from first paint (the .rv fade is
   cancelled on it) — only the green mark draws, scaleX from the left at the
   same 24x2 geometry and the same --dur-mark timing as .stn-eyebrow, which is
   the estate's micro-signature. */
.gw-datum{position:relative;height:44px;overflow:hidden}
.js .rv.gw-datum{opacity:1;transform:none}
.gw-datum::before{
  content:"";position:absolute;left:0;right:0;bottom:12px;height:1px;background:#3a3a38;
}
.gw-datum::after{
  content:"";position:absolute;left:0;right:0;bottom:13px;height:8px;
  background-image:repeating-linear-gradient(to right,#3a3a38 0 1px,transparent 1px 9px);
}
.gw-datum-mark{
  /* z-index lifts the mark over the ticks, which are the band's ::after and
     therefore paint after its children — otherwise the survey ticks rule
     straight through the one green thing on the strip. */
  position:absolute;bottom:12px;z-index:1;width:24px;height:2px;background:#7FB79C;border-radius:2px;
  left:calc(max(0px,(100% - 1180px)/2) + clamp(20px,4vw,32px));
  transform:scaleX(1);transform-origin:left center;
  transition:transform var(--dur-mark) var(--ease);
  transition-delay:calc(var(--d,0ms) + 140ms);
}
.js .rv:not(.on) .gw-datum-mark{transform:scaleX(0)}
@media (prefers-reduced-motion: reduce){
  .gw-datum-mark{transition:none !important}
  .js .rv:not(.on) .gw-datum-mark{transform:scaleX(1) !important}
}

/* ── M2 — the take-off ──────────────────────────────────────────────────────
   The ten package rows are the business's product, so they are set as a
   take-off: numbered line items, each carrying a 44px crop of the same
   engineer's section drawing, each answering the cursor in the estate's
   hairline grammar. Rows are line items, not cards — nothing lifts and
   nothing moves vertically. Rows are not links either (no href, no tabindex),
   so the hover is pure enrichment and the rest state is the whole message;
   touch and reduced motion therefore lose nothing. */

/* --- Row anatomy ----------------------------------------------------------
   44px drawing column, 18px gutter, text column. The border-top and the 14px
   padding are the shipped row rhythm, lifted off the inline styles unchanged
   so the hairline overlay has a class to sit on. */
.gw-rows{list-style:none;padding:0;margin:22px 0 0;display:flex;flex-direction:column;gap:14px}
.gw-row{
  position:relative;display:grid;grid-template-columns:44px 1fr;column-gap:18px;
  align-items:start;border-top:1px solid #E4E4E0;padding-top:14px;
}
.gw-row-head{display:flex;align-items:baseline;gap:10px}
.gw-num{
  font:700 12px Manrope,sans-serif;letter-spacing:.08em;color:#5C5C56;
  font-variant-numeric:tabular-nums;
  transition:color var(--hover-dur) var(--ease);
}
.gw-row-title{font:700 16px Manrope,sans-serif;color:#111}
.gw-row-desc{font:400 14px Manrope,sans-serif;color:#5C5C56;line-height:1.55}

/* --- The section fragments ------------------------------------------------
   Light-paper hierarchy, the inverse of the hero's: #111 held back to .58 for
   the primary profiles, #D6D6D0 for secondary structure and hatching, and one
   #2E6A4C measurement detail per fragment. The 44px box is 1 SVG unit to 1 CSS
   px, so crispness at DPR 1 is a coordinate question: a 1.75 or 1.5 stroke
   centred on a .5 coordinate lands one fully-covered device pixel with faint
   flanks, while the 2px green centred on an integer covers two pixels exactly
   — hence .5 for primary/secondary and integers for green throughout the
   markup. Square caps, as everywhere else in the estate's drawn hand. */
.gw-glyph{
  display:block;width:44px;height:44px;fill:none;
  stroke-linecap:square;stroke-linejoin:miter;
}
.gw-glyph .gw-p{stroke:#111;stroke-opacity:.58;stroke-width:1.75}
.gw-glyph .gw-s{stroke:#D6D6D0;stroke-width:1.5}
.gw-glyph .gw-m{stroke:#2E6A4C;stroke-width:2}

/* --- The hover ------------------------------------------------------------
   The estate's .card-hover top-hairline, brought down to row scale: a 2px
   green rule scaling in from the left directly over the row's own grey
   border, on --hover-dur / --ease. Fine pointers only; --hover-dur and --ease
   are the shipped tokens, so this cannot drift from the cards above it. */
.gw-row::before{
  content:"";position:absolute;left:0;right:0;top:-1px;height:2px;
  background:#2E6A4C;transform:scaleX(0);transform-origin:left center;
  transition:transform var(--hover-dur) var(--ease);
}
/* The green detail redraws on hover with the shipped pathLength="1" dash
   idiom. dasharray is declared inside the keyframe rather than on the path,
   so the rest state carries no dash machinery at all and the fragment is
   composed for no-JS, touch and reduced motion by construction. */
@keyframes gw-remeasure{
  from{stroke-dasharray:1;stroke-dashoffset:1}
  to{stroke-dasharray:1;stroke-dashoffset:0}
}
@media (hover:hover){
  .gw-row:hover::before{transform:scaleX(1)}
  .gw-row:hover .gw-num{color:#2E6A4C}
  .gw-row:hover .gw-m path{animation:gw-remeasure .45s var(--ease)}
}
/* Reduced motion: the hairline still arrives and the number still turns, but
   neither is drawn — the rule appears at full width, the green stays
   composed. */
@media (prefers-reduced-motion: reduce){
  .gw-row::before{transition:none !important;transform:scaleX(1) !important;opacity:0}
  .gw-row .gw-m path{animation:none !important}
}
@media (prefers-reduced-motion: reduce) and (hover:hover){
  .gw-row:hover::before{opacity:1}
}

/* ── M3 — the priced drawing ────────────────────────────────────────────────
   The "why specialist matters" block becomes a pair: the prose that names the
   assumptions, and one wide section drawing that marks where they bite. Two
   columns from 980px up, the drawing vertically centred against the text;
   below that it is a single column and the drawing sits between the paragraph
   and the onward cards, exactly where the eye goes next. */
.gw-why{display:grid;gap:clamp(28px,4vw,40px);align-items:center}
@media (min-width:980px){
  .gw-why{grid-template-columns:minmax(0,1fr) minmax(0,460px);gap:48px}
}

/* --- The risk section -----------------------------------------------------
   Same drawn hand as the hero and the 44px package fragments, on the
   light-paper hierarchy the fragments established. Ink weight per the
   director's M3 review: at 1440 the whole drawing read wispy against #F6F6F3
   next to the 800-weight H2, so the permanent profiles (ground, dig, footing,
   slab, construction joint, kerb) sit at #111 / .8 / 2px — the presence the M2
   glyphs have at their scale. The spoil heap and the cage are marked subjects,
   not context, so they carry primary weight too; the cage stays one step
   lighter (.7) so it still reads as steel inside concrete. Props, sub-base and
   hatch stay secondary — context stays quiet. #2E6A4C for the annotation
   layer. viewBox + aspect-ratio reserve the box from first paint, so the
   drawing costs zero CLS at every width. Square caps, as everywhere else. */
.gw-risk{
  display:block;width:100%;max-width:460px;height:auto;aspect-ratio:46/29;
  fill:none;stroke-linecap:square;stroke-linejoin:miter;
}
.gw-risk .gw-p{stroke:#111;stroke-opacity:.8;stroke-width:2}
.gw-risk .gw-c{stroke:#111;stroke-opacity:.7;stroke-width:2}
.gw-risk .gw-s{stroke:#D6D6D0;stroke-width:1.5}
.gw-risk .gw-h{stroke:#D6D6D0;stroke-width:1.25}
.gw-risk .gw-m{stroke:#2E6A4C;stroke-width:2}

/* The M1 draw machinery, re-pointed at this drawing rather than widened —
   the .gw-art rules stay byte-for-byte as shipped, exactly as M2 appended
   rather than edited. --dd runs the ground and dig layers at .45-.55s.
   The green markers deliberately do NOT take --draw-dur: this is a secondary
   moment on a page whose signature is the hero sheet, so each marker ticks in
   at --dur-mark (.35s), the estate's small-mark duration, two stagger steps
   apart — a sequence of confirmations, not a second signature. */
.js .gw-risk .pd{stroke-dasharray:1;stroke-dashoffset:1}
.js .rv.on .gw-risk .pd{
  animation:hero-draw var(--dd,var(--draw-dur)) var(--ease) forwards;
  animation-delay:calc(var(--d,0ms) + var(--i,0)*var(--stagger));
}
@media (prefers-reduced-motion: reduce){
  .js .gw-risk .pd{animation:none !important;stroke-dashoffset:0 !important}
}

/* ===========================================================================
   MOBILE EXPERIENCE (M1) — run nwcs-mobile-2026-08-20 — "one line, held
   vertical". Home hero: second, mobile-composed instance of the same scene
   (the shipped .gw-art-m mechanism). Desktop instance untouched >=881px.
   =========================================================================== */
/* Gated from inside .hero-art: the shipped `.hero-art svg{...;display:block}`
   is specificity 0,1,1, so a bare `.hero-art-m` (0,1,0) never wins the display
   cascade and BOTH instances render. Scoping to `.hero-art .hero-art-*` (0,2,0)
   is the minimum reach needed — no other declaration changes. */
.hero-art .hero-art-m{display:none}
@media (max-width:880px){
  .hero-art .hero-art-d{display:none}
  .hero-art .hero-art-m{display:block}
}
@media (max-width:480px){
  .hero-cta-row > .nwcs-cta{flex:1 1 auto;text-align:center}
}

/* ===========================================================================
   MOBILE EXPERIENCE (M2) — run nwcs-mobile-2026-08-20 — the menu as the
   index of stations: full-width sheet under the header, drawn rail, square
   ticks, descriptions restored (A-17), scrim (A-15), 44px trigger (A-14).
   =========================================================================== */
/* Geometry is declared once, here, so the rail can be positioned from the same
   numbers the grid uses — no per-row magic numbers, wrap-proof. */
.nwcs-mob .nwcs-mpanel{
  --st-px:12px;          /* row padding-inline           */
  --st-num:22px;         /* station-number column        */
  --st-gap:10px;         /* grid column gap              */
  --st-tick:8px;         /* tick size                    */
  --st-line:20px;        /* title line box               */
  --st-tick-mt:6px;      /* tick offset into that line   */
  /* distance from a row's top edge to its tick centre */
  --st-rail-c:calc(10px + var(--st-tick-mt) + var(--st-tick) / 2);
  /* horizontal centre of the tick column, from the row's left edge */
  --st-rail-x:calc(var(--st-px) + var(--st-num) + var(--st-gap) + var(--st-tick) / 2);
  position:fixed;top:61px;left:0;right:0;z-index:2;
  max-height:calc(100vh - 61px);
  max-height:calc(100dvh - 61px);
  overflow-y:auto;overscroll-behavior:contain;
  background:#fff;border-bottom:1px solid #E4E4E0;
  box-shadow:0 24px 48px -24px rgba(17,17,17,.25);
  padding:14px 20px 20px;
  flex-direction:column;
}
.nwcs-mob .nwcs-mpanel ul{list-style:none;margin:0;padding:0}

/* --- Station rows: [number] [tick] [title + description] --- */
.nwcs-mob .nwcs-strow{
  display:grid;
  grid-template-columns:var(--st-num) var(--st-tick) 1fr;
  column-gap:var(--st-gap);
  align-items:start;
  position:relative;
  min-height:44px;
  padding:10px var(--st-px);
  border-radius:8px;
}
.nwcs-mob .nwcs-st-n{
  font:600 12px/var(--st-line) Manrope,sans-serif;
  font-variant-numeric:tabular-nums;
  color:#6E6E68;
}
.nwcs-mob .nwcs-st-tick{
  width:var(--st-tick);height:var(--st-tick);border-radius:1px;
  background:#D6D6D0;margin-top:var(--st-tick-mt);
  position:relative;z-index:1;  /* sits over the rail */
}
.nwcs-mob .nwcs-st-title{
  display:block;font:700 15px/var(--st-line) Manrope,sans-serif;color:#111;
}
.nwcs-mob .nwcs-st-desc{
  display:block;font:400 12px/1.45 Manrope,sans-serif;color:#6E6E68;margin-top:1px;
}
/* the two post-divider rows carry no number: the tick keeps its column */
.nwcs-mob .nwcs-more .nwcs-st-tick{grid-column:2}
.nwcs-mob .nwcs-more .nwcs-st-txt{grid-column:3}

/* --- The rail: one 2px rule through the five station tick centres.
   Drawn as a per-row segment on the row itself (not the <li>) so it paints
   above the row's hover wash, and trimmed at the ends by the same
   tick-centre constant — so a wrapping description can never misalign it. --- */
.nwcs-mob .nwcs-stations .nwcs-strow::before{
  content:"";position:absolute;
  left:var(--st-rail-x);margin-left:-1px;width:2px;
  top:0;bottom:0;background:#D6D6D0;
}
.nwcs-mob .nwcs-stations li:first-child .nwcs-strow::before{top:var(--st-rail-c)}
.nwcs-mob .nwcs-stations li:last-child .nwcs-strow::before{bottom:auto;height:var(--st-rail-c)}

/* --- Current page: green tick + green number. Rest state stays white. --- */
.nwcs-mob .nwcs-strow[aria-current] .nwcs-st-tick{background:#2E6A4C}
.nwcs-mob .nwcs-strow[aria-current] .nwcs-st-n{color:#2E6A4C}

/* --- Divider --- */
.nwcs-mob .nwcs-mdiv{height:1px;background:#E4E4E0;margin:8px 0}

/* --- CTA sits full width at the foot of the sheet --- */
.nwcs-mob .nwcs-mpanel > .nwcs-cta{display:block;width:100%}

/* --- Scrim (A-15). Fixed descendants of .nwcs-hdr resolve against the header
   (its backdrop-filter makes it their containing block), so the height is
   stated explicitly rather than via inset/bottom, which would collapse. --- */
.nwcs-mob[open]::before{
  content:"";position:fixed;z-index:1;
  left:0;right:0;top:61px;
  height:calc(100vh - 61px);
  height:calc(100dvh - 61px);
  background:rgba(17,17,17,.32);
  animation:nwcs-scrim-in var(--hover-dur) var(--ease);
}
@keyframes nwcs-scrim-in{from{opacity:0}to{opacity:1}}

@media (prefers-reduced-motion: reduce){
  .nwcs-mob[open]::before{animation:none}
}

/* ===========================================================================
   MOBILE EXPERIENCE (Milestone 3, commission 2026-08-20) — the section threads
   on the phone. Above 1120 the four-card sections carry a drawn horizontal
   rail (estimating, live) or the held line with drop-ticks (order). Below it
   the grid stacks and both threads were dropped, so the phone lost the line
   that the whole "one line, four stations" idea rests on. These rules
   re-express the same thread vertically, in the estate's shipped grammar:
   a 2px grey rule down the cards' left padding zone, one square node per card
   drawn scaleY/scale on the card's own reveal — the .how-step::before and
   .stn-spine idiom turned through 90 degrees. Append-only, .stn-* namespaced.

   BREAKPOINT: the commission said "<1120". Measured, the auto-fit grid
   (minmax(250px,1fr)) is one column only up to 560px — it is 2-up from 561,
   3-up from ~846 and 4-up from ~1081, while the horizontal rail stays off
   until 1120. A vertical thread across a multi-column grid is not one thread:
   it splits into one rule per column, threads 1->3 / 2->4 instead of reading
   order, and the bottom-row column-1 segment dangles 31px into the section
   below — which the commission's own "the rail shouldn't dangle past the last
   card" rule forbids. So these rules are gated to the single-column range,
   on exactly the doctrine the shipped .stn-cardrail block states: a rail is
   only drawn where the grid makes it true. 561-1119 keeps shipped behaviour.
   =========================================================================== */

/* The node span exists in the markup on every viewport, so it is display:none
   by default and only switched on below the breakpoint. Nothing here can
   reach a >=561px layout. */
.stn-vnode{display:none}

@media (max-width:560px){
  .stn-vcard{position:relative}

  /* --- Rail segment ---------------------------------------------------------
     One segment per card, carried by the card itself so it draws on the card's
     own reveal. Geometry: the cards carry 24px inline padding, so the rail sits
     at x16-18 (centre 17) — the same 16px inset as the card's shipped top
     hairline (.stn-card::before), 6px clear of the text column. The commission
     named 18px against an assumed 28px padding; at the real 24px that leaves
     4px and crowds, so this takes the 14-16px latitude the spec granted.
     top:14px starts it at the node; bottom:-32px is the 16px grid row-gap plus
     the two 1px card borders it crosses plus the next card's own 14px inset —
     measured, not assumed — so each segment lands exactly on the next card's
     node and the stack reads as one continuous rule. */
  .stn-vcard::after{
    content:"";position:absolute;left:16px;top:14px;bottom:-32px;width:2px;
    background:#D6D6D0;border-radius:2px;
    transform-origin:top center;
    transition:transform .5s var(--ease);
    transition-delay:calc(var(--d,0ms) + 120ms);
  }
  .js .stn-vcard:not(.on)::after{transform:scaleY(0)}

  /* Last card (estimating): a take-off has no end, so there is no terminal
     node — but the rail must not dangle past the stack into the section below,
     so its segment closes on its own bottom inset. */
  .stn-vcard:last-child::after{bottom:14px}
  /* Last card (live): the line does not continue past the close-out node. The
     segment arriving from card 3 terminates at the dot. */
  .stn-vcard-last::after{display:none}

  /* --- Node ----------------------------------------------------------------
     8px square, the shipped .stn-cardrail-node grammar. left:13px centres it on
     the rail centreline (13 + 4 = 17). Above the segment in paint order so the
     green tick is never overpainted by the grey rule. Grey until the card's
     .on, then green — green draws last, after the segment. */
  .stn-vnode{
    display:block;position:absolute;left:13px;top:14px;width:8px;height:8px;
    border-radius:1px;background:#D6D6D0;opacity:1;z-index:1;
    transition:transform var(--dur-mark) var(--ease),background-color var(--dur-mark) var(--ease),opacity var(--dur-mark) var(--ease);
    transition-delay:calc(var(--d,0ms) + 260ms);
  }
  .stn-vcard.on .stn-vnode{background:#2E6A4C}
  .js .stn-vcard:not(.on) .stn-vnode{transform:scale(0);opacity:0}

  /* The close-out dot (live only). Same .stn-stop-current geometry trick the
     station rail uses: 11px with -1.5px margins, so the centre stays on the
     rail centreline (17) and the node's 8px layout box is unchanged. */
  .stn-vnode-dot{
    width:11px;height:11px;margin-left:-1.5px;margin-top:-1.5px;border-radius:50%;
  }

  /* --- Order's held cards ---------------------------------------------------
     The shipped block kills the drop-ticks below 1120 with content:none,
     because the cards no longer sit under the line. Stacked, the same meaning
     survives if each tick hangs at the card's LEFT instead of its centre, on
     the vertical-rail x of the two sibling pages: the stack reads as cards hung
     in a line. Only position changes — the 2px width, the green, the
     top-origin scaleY draw, its transition and its delay all live on the base
     rule and survive this override. Reach is measured, not assumed: card 1
     hangs 30px from the .stn-held line's own bottom margin (31px from its own
     padding-box top, across its 1px border), cards 2-4 hang the 16px grid
     row-gap plus the two 1px card borders. Ticks touch; nothing floats. */
  .stn-held-card::after{
    content:"";left:16px;margin-left:0;top:-17px;height:17px;
  }
  .stn-held-card:first-child::after{top:-31px;height:31px}
}

/* Reduced motion: the composed end-state, stated outside the media query and
   with !important so it beats the .js pre-states regardless of source order.
   A reduced-motion visitor may never scroll, so composure cannot depend on
   .on — the transforms are neutralised on the :not(.on) pre-state itself,
   exactly as the shipped .stn-cardrail block does. */
@media (prefers-reduced-motion: reduce){
  .stn-vcard::after,.stn-vnode{transition:none !important}
  .js .stn-vcard:not(.on)::after{transform:none !important}
  .js .stn-vcard:not(.on) .stn-vnode{transform:none !important;opacity:1 !important}
}

/* ===========================================================================
   MOBILE EXPERIENCE (M4) — run nwcs-mobile-2026-08-20 — the docket: the
   composer's consequence rides with the thumb (<880, contact only, JS-only).
   =========================================================================== */
/* Fixed overlay: it is appended to <body> after paint and never takes part in
   layout, so it contributes no shift. z-index 90 keeps it above page content
   and below the header (z 100, a stacking context of its own) — so the open
   menu's scrim and panel still cover it. */
.bb-docket{
  position:fixed;left:0;right:0;bottom:0;z-index:90;
  display:flex;align-items:center;gap:12px;
  padding:10px 16px calc(10px + env(safe-area-inset-bottom));
  background:#111;border-top:1px solid #3a3a38;
  transform:translateY(102%);opacity:0;pointer-events:none;
  transition:transform var(--hover-dur) var(--ease),opacity var(--hover-dur) var(--ease);
}
.bb-docket.on{transform:none;opacity:1;pointer-events:auto}

/* The open menu owns the screen. z-index 90 already puts the bar under the
   menu's scrim, and the scrim does take the taps — but the scrim is 32% black
   and the bar is #111, so covering it changes nothing a visitor can see: the
   docket would still read as live under an open menu. So it withdraws for the
   duration, on the same slide it arrived by. */
html.nwcs-menu-open .bb-docket{transform:translateY(102%);opacity:0;pointer-events:none}

/* The four group ticks — the estate's 8px square mark, one per question. */
.bb-docket-ticks{display:flex;align-items:center;gap:6px;flex:0 0 auto}
.bb-docket-tick{
  width:8px;height:8px;border-radius:1px;background:#3a3a38;
  transition:background-color var(--dur-mark) var(--ease);
}
.bb-docket-tick.on{background:#2E6A4C}

/* The answer just given. min-width:0 is what actually lets a flex item
   truncate — without it the text node keeps its content width and the bar
   overflows instead of ellipsing. */
.bb-docket-txt{
  flex:1 1 auto;min-width:0;
  font:500 13px Manrope,sans-serif;color:#f4f4ef;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
}

.bb-docket-jump{
  flex:0 0 auto;appearance:none;cursor:pointer;
  min-height:44px;padding:0 14px;
  border:1px solid #3a3a38;border-radius:8px;background:transparent;
  color:#f4f4ef;font:700 13px Manrope,sans-serif;
  transition:border-color var(--hover-dur) var(--ease),background-color var(--hover-dur) var(--ease);
}
@media (hover:hover){
  .bb-docket-jump:hover{border-color:#7FB79C}
}
.bb-docket-jump:focus-visible{outline:2px solid #7FB79C;outline-offset:2px}

/* The docket is the phone's answer only. */
@media (min-width:880px){
  .bb-docket{display:none}
}

@media (prefers-reduced-motion: reduce){
  .bb-docket,.bb-docket-tick,.bb-docket-jump{transition:none}
}

/* ============================================================
   Milestone M5 — remaining tap-target floors (snag A-14)
   ------------------------------------------------------------
   Every remaining interactive element below the 44px touch floor
   is grown by vertical padding, with an equal NEGATIVE vertical
   margin so the element's MARGIN box — the box flex/inline layout
   actually measures — is unchanged. Text baselines, column pitch
   and the footer's visual rhythm therefore stay pixel-identical;
   only the hit box grows, upward and downward into the gaps.

   Centre-tap safety: the footer/contact columns run on a 29px
   pitch, so a 45px box reaches +/-22.5px from its own centre and
   stops 6.5px short of its neighbour's centre. Every link's visual
   centre still resolves to itself under elementFromPoint, so the
   overlap in the gaps is harmless. (Route taken: negative-margin
   compensation — the footer's pitch is NOT opened.)

   .link-uline / footer underline hairlines are absolutely
   positioned against the PADDING box, so their `bottom` is
   re-offset by the padding they now sit inside.
   ============================================================ */

/* --- Stacked (blockified flex-item) links: footer nav columns,
       footer contact lines, privacy contact block. 19-21px text
       -> 45-47px effective, layout-neutral. --- */
.nwcs-ftr a:not(.nwcs-cta),
.nwcs-ftr .tap-block,
.tap-block{
  padding-top:13px;padding-bottom:13px;
  margin-top:-13px;margin-bottom:-13px;
}
.nwcs-ftr a:not(.nwcs-cta)::after,
.tap-block.link-uline::after{bottom:10px}

/* --- Inline links: breadcrumb trails and the privacy prose links.
       Vertical padding on an inline box grows the hit area without
       touching the line box, so no margin compensation is needed
       and the crumb row does not move. 17px -> 45px. --- */
.tap-inline{padding-top:14px;padding-bottom:14px}
.tap-inline.link-uline::after{bottom:11px}

/* --- Header logo lockup: 26px -> 46px, vertically centred in the
       header row exactly as before. --- */
.tap-logo{
  padding-top:10px;padding-bottom:10px;
  margin-top:-10px;margin-bottom:-10px;
}

/* ============================================================
   404 — "Walking the line" (Direction C).  APPEND-ONLY block;
   every selector is namespaced .nf-* so nothing above changes.
   Estate reuse: .stn-eyebrow (recovery eyebrow), .nwcs-cta /
   .nwcs-cta-solid (CTA row), .link-uline + .tap-inline (quick
   links).  Only the pieces with no estate equivalent — the
   stage, the world layers, the station labels and the maker's
   mark — are replicated here.
   ============================================================ */

/* --- Stage ---------------------------------------------------------------
   Height is reserved by CSS from first paint, so swapping the static
   drawing for the interactive world costs no layout shift (CLS 0).
   Measured sticky header: 69px above the burger breakpoint, 73px below.
   svh with a vh fallback for browsers without the small-viewport unit. */
.nf-stage{
  position:relative;
  overflow:hidden;
  background:#FBFBF9;
  min-height:calc(100vh - 69px);
  min-height:calc(100svh - 69px);
  touch-action:pan-y;
}
@media (max-width:880px){
  .nf-stage{
    min-height:calc(100vh - 73px);
    min-height:calc(100svh - 73px);
  }
}
.nf-stage:focus{outline:none}

/* --- Recovery block (invariant 1) ----------------------------------------
   Absolute inside the stage: the world slides underneath, this never pans
   and never animates. The wrapper is inert so a drag can start anywhere the
   card is not; the card itself takes pointers back. The feather is a
   background gradient only — no backdrop-filter. */
.nf-recovery{
  position:absolute;top:0;left:0;z-index:4;
  max-width:560px;
  padding:clamp(20px,4vw,32px);
  pointer-events:none;
}
.nf-recovery::before{
  content:"";position:absolute;left:0;top:0;width:155%;height:165%;
  z-index:-1;pointer-events:none;
  background:radial-gradient(115% 92% at 0% 0%,
    rgba(251,251,249,.92) 0%,
    rgba(251,251,249,.92) 42%,
    rgba(251,251,249,0) 100%);
}
.nf-rec-card{pointer-events:auto}
.nf-eyebrow{
  margin:0;font:700 12px Manrope,sans-serif;
  letter-spacing:.1em;text-transform:uppercase;color:#2E6A4C;
}
.nf-h1{
  margin:16px 0 0;
  font:800 clamp(30px,4.6vw,52px)/1.05 Manrope,sans-serif;
  letter-spacing:-.03em;color:#111;max-width:15ch;
}
.nf-body{
  margin:18px 0 0;
  font:400 17px/1.6 Manrope,sans-serif;color:#5C5C56;max-width:46ch;
}
.nf-ctas{display:flex;flex-wrap:wrap;gap:12px;margin-top:28px}
.nf-quick{display:flex;flex-wrap:wrap;gap:8px 20px;margin-top:22px}
.nf-quick-a{font:700 15px Manrope,sans-serif;color:#2E6A4C}

/* --- Static composed drawing = the no-JS page (invariant 5) --------------- */
.nf-static{
  position:absolute;left:0;right:0;z-index:2;
  bottom:clamp(20px,4vh,44px);
  padding:0 clamp(20px,4vw,32px);
}
.nf-static-inner{max-width:1180px;margin:0 auto}
.nf-static-art svg{display:block;width:100%;height:auto}
.nf-static-row{
  display:flex;flex-wrap:wrap;align-items:center;
  gap:10px 20px;margin-top:18px;
}
/* The .js gate: site.js adds .nf-live once the world has actually booted,
   so a JS failure leaves the static page standing. */
.js .nf-stage.nf-live .nf-static{display:none}
.js .nf-stage.nf-live{cursor:grab}
.js .nf-stage.nf-live.nf-dragging{cursor:grabbing}

/* --- Station label (eyebrow grammar, replicated: the estate .stn-eyebrow
       hangs its rule off the text box, and these need a 44px hit area) --- */
.nf-stn,.nf-lbl{
  position:relative;display:inline-block;
  padding:11px 0 21px;
  font:700 12px Manrope,sans-serif;
  letter-spacing:.1em;text-transform:uppercase;color:#111;
}
.nf-stn::after,.nf-lbl::after{
  content:"";position:absolute;left:0;bottom:11px;
  width:24px;height:2px;background:#2E6A4C;border-radius:2px;
}

/* --- Maker's mark (§6) --------------------------------------------------- */
.nf-mark{
  position:relative;display:block;flex:0 0 auto;
  width:200px;height:64px;margin-left:auto;
}
.nf-mark-art{position:absolute;left:0;top:0;width:200px;height:64px;display:block}
.nf-mark-i{transition:stroke var(--hover-dur) var(--ease)}
.nf-mark-txt{
  position:absolute;left:0;top:0;right:0;bottom:0;
  display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;
}
.nf-mark-1{font:600 12px Manrope,sans-serif;letter-spacing:.1em;text-transform:uppercase;color:#6E6E68}
.nf-mark-2{font:700 14px Manrope,sans-serif;letter-spacing:.08em;text-transform:uppercase;color:#111}
.nf-mark:focus-visible .nf-mark-i{stroke:#2E6A4C}
@media (hover:hover){
  .nf-mark:hover .nf-mark-i{stroke:#2E6A4C}
}

/* --- Interactive world layers (built by site.js) ------------------------- */
.nf-grid{
  position:absolute;top:0;left:0;z-index:1;
  opacity:.35;pointer-events:none;will-change:transform;
}
.nf-grid svg{display:block}
.nf-world{position:absolute;top:0;left:0;z-index:2;will-change:transform}
.nf-world > svg{position:absolute;top:0;left:0;display:block;shape-rendering:geometricPrecision}
.nf-ovl{position:absolute;top:0;left:0}
.nf-lbl{
  position:absolute;white-space:nowrap;text-decoration:none;
  opacity:0;transform:translate(-50%,var(--rise));
}
.nf-lbl::after{left:12px}
.nf-lbl{padding-left:12px;padding-right:12px}
.nf-hint{
  position:absolute;white-space:nowrap;opacity:0;
  font:700 12px Manrope,sans-serif;
  letter-spacing:.1em;text-transform:uppercase;color:#6E6E68;
  transform:translateY(-50%);
}

/* --- Reduced motion: the world renders composed; only the motion goes.
       The JS half boots composed too (see site.js). ---------------------- */
@media (prefers-reduced-motion: reduce){
  .nf-world,.nf-grid{transition:none !important}
  .nf-stage [data-nf-anim]{transition:none !important}
  .nf-lbl{opacity:1 !important;transform:translate(-50%,0) !important;transition:none !important}
  .nf-hint{opacity:1 !important;transition:none !important}
  .nf-mark-i{transition:none !important}
}

/* ============================================================
   404 M2 — full feel.  APPEND-ONLY; every selector is .nf-*, and
   the only rules it overrides are its own M1 block above.
   ============================================================ */

/* --- Composition: the stage is a column ----------------------------------
   The recovery block and the static composed drawing sit in normal flow, so
   they can never collide at any viewport; the grid and world layers stay
   absolutely positioned, so the swap to the interactive world still costs no
   layout shift.  The drawing shrinks into whatever air is left below the
   recovery block (flex, with a floor), and only when even the floor will not
   fit does the stage grow past its min-height — nothing is ever clipped. */
.nf-stage{display:flex;flex-direction:column}
.nf-recovery{position:relative;flex:0 0 auto}
.nf-static{
  position:relative;bottom:auto;
  flex:1 1 auto;min-height:0;
  display:flex;flex-direction:column;justify-content:flex-end;
  margin-top:clamp(20px,3.4vh,40px);
  padding-bottom:clamp(20px,4vh,44px);
}
.nf-static-inner{width:100%;flex:1 1 auto;min-height:0;display:flex;flex-direction:column}
.nf-static-art{
  position:relative;flex:1 1 auto;
  max-height:270px;                     /* the drawing's own height, never upscaled */
  min-height:min(150px,20vw);           /* a floor, never taller than it can fill */
}
.nf-static-art svg{position:absolute;left:0;top:0;width:100%;height:100%}
.nf-static-row{flex:0 0 auto}
/* Live: the static half keeps its box (so CLS stays 0) but leaves sight,
   hit-testing and the tab order — which is what guarantees that exactly one
   maker's mark is visible in each render mode. */
.js .nf-stage.nf-live .nf-static{display:flex;visibility:hidden}

/* --- Recovery block: CTA row and the quick-links grid ---------------------
   CTAs: 16px gutter, and they share a row only while both still fit — below
   that they wrap and each takes the full width, still >=44px tall.
   Quick links: a deliberate 2-column grid (2x2 at 390) with one horizontal
   and one vertical gutter, instead of the ragged wrap of two flex rows. */
.nf-ctas{gap:16px}
.nf-quick{
  display:grid;grid-template-columns:repeat(2,minmax(0,1fr));
  gap:8px 24px;align-items:start;margin-top:20px;
}
@media (max-width:560px){
  .nf-ctas > a{flex:1 1 auto;text-align:center}
  .nf-quick{gap:8px 16px}
}

/* --- The maker's mark in the interactive world (§6) ----------------------- */
.nf-ovl .nf-mark{position:absolute;margin-left:0}
.nf-mark-txt{transition:none}

/* Narrow stages: the static station row becomes a 2-column grid of 44px
   targets (the same treatment §7 gives the quick links), so the no-JS page
   composes in the stage band instead of stacking five full-width rows. */
@media (max-width:560px){
  .nf-static-row{
    display:grid;grid-template-columns:repeat(2,minmax(0,1fr));
    gap:10px 16px;align-items:start;
  }
  .nf-static-row .nf-mark{grid-column:1 / -1;justify-self:end}
}

/* --- Grimsdale footer credit — package standard, brand-fixed (do not restyle) --- */
.gwd-credit{
  box-sizing:border-box;
  background:var(--gwd-bg);
  display:flex; flex-direction:column; align-items:center; gap:10px;
  padding:18px 24px 16px; text-align:center;
  font-family:system-ui,-apple-system,"Segoe UI",sans-serif;
}
.gwd-credit__link{ position:relative; display:inline-flex; align-items:center; gap:11px; text-decoration:none; }
.gwd-credit__by{ font-size:13px; font-weight:500; letter-spacing:.01em; color:var(--gwd-fg); }
.gwd-credit__link svg{ height:24px; width:auto; display:block; }
.gwd-credit__link::after{
  content:""; position:absolute; left:0; right:0; bottom:-6px; height:1.5px;
  background:#D5331E; transform:scaleX(0); transform-origin:left;
  transition:transform .3s cubic-bezier(.6,.1,.2,1);
}
.gwd-credit__link:hover::after{ transform:scaleX(1); }
.gwd-credit__copy{ margin:0; font-size:12px; color:var(--gwd-copy); }


/* ============================================================
   404 M3 — bounded composition pass (spec §9).  APPEND-ONLY;
   every selector is .nf-*, and the only rules it overrides are
   its own M1/M2 blocks above.  Nothing here touches physics,
   the drawing law or the recovery structure.
   ============================================================ */

/* --- §9.3  The recovery ground -------------------------------------------
   The sheet the notes are written on.  The block itself sits on a flat
   #FBFBF9 ground at 92% (the §2 figure), so ANY world line-work panning
   beneath it — including the quick-links band, which the M1 radial left in
   the falloff — reads at 8% of its strength.  The two open sides (right and
   bottom; top and left are the stage's own edges) get a 64px feather, built
   as three non-overlapping background layers so the corner never doubles its
   alpha: a right band, a bottom band, and a radial quadrant between them.
   Background only — no backdrop-filter. */
.nf-recovery::before{
  width:100%;height:100%;
  background:rgba(251,251,249,.92);
}
.nf-recovery::after{
  content:"";position:absolute;z-index:-1;pointer-events:none;
  left:0;top:0;right:-64px;bottom:-64px;
  background:
    linear-gradient(to right,rgba(251,251,249,.92) 0%,rgba(251,251,249,0) 100%)
      right top/64px calc(100% - 64px) no-repeat,
    linear-gradient(to bottom,rgba(251,251,249,.92) 0%,rgba(251,251,249,0) 100%)
      left bottom/calc(100% - 64px) 64px no-repeat,
    radial-gradient(circle 64px at 0% 0%,rgba(251,251,249,.92) 0%,rgba(251,251,249,0) 100%)
      right bottom/64px 64px no-repeat;
}

/* --- §9.2  Stage height at <=560 ------------------------------------------
   The static station links keep the 2-column grid (M2) but stop reserving
   the tall two-line rows: each target is a 44px floor with the text centred
   in it and the eyebrow rule re-hung inside the shorter box.  One box model
   in both render modes still, so CLS stays 0. */
@media (max-width:560px){
  .nf-static-row .nf-stn{
    display:flex;align-items:center;
    box-sizing:border-box;min-height:44px;
    padding:0 0 12px;
  }
  .nf-static-row .nf-stn::after{bottom:5px}
}


/* --- Grimsdale footer credit — "Red pen" flourish (scoped to .gwd-credit) --- */
.gwd-credit{position:relative;overflow:hidden;overflow:clip}
.gwd-credit__link{touch-action:pan-y}
.gwd-credit__pen{position:absolute;inset:0;width:100%;height:100%;pointer-events:none;z-index:2;overflow:visible}
.gwd-credit__ink{fill:none;stroke:#D5331E;stroke-width:2;stroke-linejoin:round;stroke-linecap:round}
.gwd-credit__nib{fill:#D5331E}
@media (prefers-reduced-motion:reduce){.gwd-credit__pen{display:none}}


/* ============================================================
   404 M5 — engineering only.  APPEND-ONLY; .nf-* namespaced.
   The world is drawn into per-span chunk layers (see site.js):
   one <svg> per drawing layer per 480px span, so a stroke-draw
   dirties a 480px-wide paint artifact instead of the whole
   7680px world.  Nothing here changes a colour, a size, a
   duration or an easing curve.
   ============================================================ */
.nf-c{
  position:absolute;top:0;display:block;
  overflow:visible;                    /* a feature may reach past its own span */
  shape-rendering:geometricPrecision;
  contain:layout style;                /* isolate: never clips (no paint containment) */
}
