/* ============================================================================
   Delta Precision Group — global.css
   Loaded on every page. Concatenated source split (build spec §7):
      tokens → reset → base → layout → components → utilities
   RULE: page-scoped CSS must never hardcode a hex literal — use these tokens.
   Brand direction locked from client mockups (Sep 2026): cyan/teal accents,
   navy grid hero, Raleway / DM Sans, lime CTA. (Headings and
   body switched from Space Grotesk / Inter on 2026-10-05, client — the old
   woff2 files stay in assets/fonts/ unreferenced, kept only to switch back.
   JetBrains Mono dropped 2026-10-15, client: labels are DM Sans, and code
   falls back to the system monospace stack, so no mono font is downloaded.)
   ============================================================================ */

/* ----------------------------------------------------------------------------
   0. FONTS (self-hosted woff2, assets/fonts) — font-display:swap kills FOIT.
   Paths are relative to this file (assets/css/ → ../fonts/).
---------------------------------------------------------------------------- */
@font-face{font-family:"Raleway";src:url("../fonts/raleway-v37-latin-500.woff2") format("woff2");font-weight:500;font-style:normal;font-display:swap;}
@font-face{font-family:"Raleway";src:url("../fonts/raleway-v37-latin-700.woff2") format("woff2");font-weight:700;font-style:normal;font-display:swap;}
@font-face{font-family:"DM Sans";src:url("../fonts/dm-sans-v17-latin-regular.woff2") format("woff2");font-weight:400;font-style:normal;font-display:swap;}
@font-face{font-family:"DM Sans";src:url("../fonts/dm-sans-v17-latin-500.woff2") format("woff2");font-weight:500;font-style:normal;font-display:swap;}
@font-face{font-family:"DM Sans";src:url("../fonts/dm-sans-v17-latin-600.woff2") format("woff2");font-weight:600;font-style:normal;font-display:swap;}
@font-face{font-family:"DM Sans";src:url("../fonts/dm-sans-v17-latin-700.woff2") format("woff2");font-weight:700;font-style:normal;font-display:swap;}

/* ----------------------------------------------------------------------------
   1. TOKENS — the shared contract. Decided once; no page overrides a token.
---------------------------------------------------------------------------- */
:root{
  /* -- brand / accent -------------------------------------------------- */
  --dpg-cyan:#24c7d9;          /* accent on DARK sections (hero, CTA band)   */
  --dpg-teal:#0798aa;          /* accent on LIGHT sections (body of page)     */
  --dpg-lime:#c9f36b;          /* primary CTA button                         */
  --dpg-lime-hover:#d6f77e;    /* lightened lime, primary CTA hover          */
  --dpg-lime-tint:#d2f69f;     /* pale lime SURFACE — "What we deliver" cards (client, Step 15v). A ground, never text */
  --dpg-lime-wash:#e3ffb3;     /* paler lime SURFACE — GIS "Our services" cards, borderless (client, Step 16d). Never text */
  --dpg-signal:#5fd38f;
  --dpg-danger:#b42318;        /* form errors — 6.5:1 on white, 6.2:1 on --dpg-slate-50 (Step 19i) */
  /* Teal for TEXT on a light surface. --dpg-teal is the brand fill (icons,
     chips, rules) but only reaches 3.1-3.45:1 as text and fails AA at body
     and label sizes; this darker step is 4.98:1 on the cyan band and 5.5:1 on
     white. Client's own case-study mockup uses exactly this value. */
  --dpg-teal-ink:#0e7380;        /* live "systems online" status dot           */

  /* -- dark backgrounds ------------------------------------------------ */
  --dpg-navy:#0b1b2b;          /* primary dark background (hero, CTA)        */
  --dpg-navy-deep:#0f172a;     /* alternate deep navy (slate-900)            */
  --dpg-ink:#061a22;           /* deeper, cooler dark ground — see .dpg-section--ink */
  --dpg-grid-line:#24c7d917;   /* grid lines on DARK bands (cyan @ ~9%)      */
  --dpg-grid-line-light:#0a192912; /* same grid on LIGHT bands (navy @ ~7%) */
  --dpg-dot-line:#ffffff33;    /* DOT for the blog banner's texture — white at
                                  20%, matching the reference SVG's opacity=0.2.
                                  White rather than cyan because both references
                                  lift the ground equally on all three channels:
                                  a neutral lightening, not a hue. The SVG draws
                                  BLACK at .2 because that site is light-ground;
                                  inverted here, the alpha is the same.        */

  /* -- neutral / slate ramp (adopted wholesale) ----------------------- */
  --dpg-white:#ffffff;
  --dpg-slate-50:#f8fafc;      /* light section backgrounds                 */
  --dpg-slate-100:#f1f5f9;     /* tinted callouts                           */
  --dpg-slate-200:#e2e8f0;     /* universal border colour                   */
  --dpg-slate-300:#cbd5e1;     /* light outline button border               */
  --dpg-slate-400:#94a3b8;
  --dpg-slate-500:#64748b;
  --dpg-slate-600:#475569;
  --dpg-slate-700:#334155;
  --dpg-slate-800:#1e293b;
  --dpg-slate-900:#0f172a;

  /* -- semantic text --------------------------------------------------- */
  --dpg-heading:#0a1929;             /* section headings on light bg        */
  --dpg-text:#4a5a6e;                /* body copy on light bg               */
  --dpg-heading-on-dark:#d2dee3;     /* headings on dark sections           */
  --dpg-text-on-dark:#ffffff;        /* hero H1 / strong text on dark       */
  --dpg-text-on-dark-muted:#94a3b8;  /* hero supporting / muted on dark     */

  /* -- surfaces & lines ------------------------------------------------ */
  --dpg-tint-cyan:#e8f5f7;     /* case-study band; the one non-slate surface */
  /* Comparison columns. Text first, surface second; the pairs are 8.8:1 and
     7.7:1, so the note inside a cell can step back to .85 opacity and still
     clear AA. These are the only non-slate, non-brand hues on the site. */
  --dpg-without-text:#6b3a3a;
  --dpg-without-bg:#fff9f9;
  --dpg-with-text:#2d5a2d;
  --dpg-with-bg:#f5fbf5;
  --dpg-navbar-bg:rgba(245,248,250,.92);
  --dpg-border:var(--dpg-slate-200);
  --dpg-border-on-dark:rgba(255,255,255,.14);

  /* -- fonts ----------------------------------------------------------- */
  --dpg-font-heading:"Raleway", system-ui, sans-serif;
  --dpg-font-body:"DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  --dpg-font-mono:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; /* code only — system fonts, nothing downloaded */

  /* -- type scale (fluid clamps; desktop maxima match the mockups) ----- */
  --dpg-fs-h1:clamp(2.25rem, 1.35rem + 3.9vw, 3.5rem);   /* hero, → 56px    */
  --dpg-fs-h2:clamp(1.75rem, 1.15rem + 1.7vw, 2.25rem);  /* section, → 36px */
  --dpg-fs-h3:clamp(1.25rem, 1.05rem + 0.7vw, 1.5rem);   /* → 24px          */
  --dpg-fs-lead:1.15rem;        /* hero supporting text (18.4px)            */
  --dpg-fs-body:1.1rem;         /* body copy (17.6px)                       */
  --dpg-fs-small:0.9375rem;     /* 15px                                     */
  --dpg-fs-label:0.75rem;       /* mono badges / eyebrows (12px)            */
  --dpg-fs-stat:clamp(2rem, 1.4rem + 2.2vw, 2.75rem);
  /* One hero metric owning a whole card needs a step above --dpg-fs-stat,
     which peaks at 44px and is sized for the 4-across proof strip. Peaks
     120px, against ~125px measured off the client's case-study mockup. */
  --dpg-fs-stat-xl:clamp(3.5rem, 2rem + 6vw, 7.5rem); /* proof figures, → 44px */

  --dpg-lh-tight:1.1;           /* hero H1                                  */
  --dpg-lh-heading:1.2;         /* section headings                        */
  --dpg-lh-lead:1.7;            /* hero supporting                          */
  --dpg-lh-body:1.8;            /* body copy (31.68/17.6)                   */

  /* -- spacing --------------------------------------------------------- */
  --dpg-gutter:16px;                 /* steps up at 640 / 1024 below        */
  --dpg-container:1280px;
  --dpg-section-pad:80px;
  --dpg-cta-pad:96px;

  /* -- hero photo placement (tuned on a 1536px screen) ------------------ */
  --dpg-hero-media-start:333px;  /* photo begins this far right of the container's left edge */
  --dpg-hero-media-fade:260px;   /* left fade length; fixed, NOT a % (see §5.5) */

  /* A tier above every heading, for a numeral that IS the artwork rather than
     a heading — currently only the 404's "4 0 4". Same justification as
     --dpg-fs-stat-xl: the mockup needed a size the scale did not have, so the
     SCALE grew a step rather than the class growing a hardcoded px value
     (field guide §1.4). Peaks 208px, floors at 80px. */
  --dpg-fs-display:clamp(5rem, 2rem + 14vw, 13rem);

  /* -- motion ---------------------------------------------------------- */
  /* One source of truth for interaction timing. `.18s ease` had been copied
     into five rules by hand before these existed (Step 10ad); a duration is a
     design value like any other and belongs on a scale, not in the rule.
     --fast is state feedback the eye should barely register (colour, rotation);
     --med is something that physically moves and needs to be followed (the
     mobile accordion opening). No --slow: nothing here should be slower.
     prefers-reduced-motion is handled ONCE, globally, in §2 — do not add local
     `transition:none` rules, they are already covered. */
  --dpg-t-fast:.18s;
  --dpg-t-med:.26s;
  --dpg-ease:cubic-bezier(.4,0,.2,1);

  /* -- radii ----------------------------------------------------------- */
  --dpg-r-xs:2px; --dpg-r-sm:4px; --dpg-r-md:8px; --dpg-r-lg:12px;
  --dpg-r-xl:16px; --dpg-r-pill:999px;

  /* -- shadows (soft, low-opacity, multi-layer) ------------------------ */
  --dpg-shadow-sm:0 1px 2px rgba(10,25,41,.05), 0 1px 3px rgba(10,25,41,.06);
  --dpg-shadow-md:0 2px 4px rgba(10,25,41,.04), 0 8px 20px rgba(10,25,41,.08);
  --dpg-shadow-lg:0 12px 32px rgba(10,25,41,.12), 0 4px 8px rgba(10,25,41,.06);

  /* -- z-index --------------------------------------------------------- */
  --dpg-z-header:100;
  --dpg-z-anchor:90;
}
@media (min-width:640px){ :root{ --dpg-gutter:24px; } }
@media (min-width:1024px){ :root{ --dpg-gutter:32px; } }

/* ----------------------------------------------------------------------------
   2. RESET (minimal; neutralises the defaults we care about)
---------------------------------------------------------------------------- */
*,*::before,*::after{box-sizing:border-box;}
html{-webkit-text-size-adjust:100%;}
body{margin:0;}
/* dl/dd joined this list at Step 10g, when .dpg-service introduced the site's
   first description list. Browsers give dl a block margin and dd a 40px inline
   start indent; both would fight a grid-laid dl. Reset here rather than inside
   the component, so any future <dl> is safe by default. */
h1,h2,h3,h4,p,figure,dl,dd{margin:0;}
img,svg,video{display:block;max-width:100%;height:auto;}
a{color:inherit;text-decoration:none;}
button{font:inherit;color:inherit;cursor:pointer;}
ul,ol{margin:0;padding:0;list-style:none;}

/* ----------------------------------------------------------------------------
   3. BASE (page defaults, headings, links, focus)
---------------------------------------------------------------------------- */
/* Build spec §8 budgets "CSS scroll-behavior:smooth + scroll-margin-top on
   section IDs so the sticky anchor bar does not overlap headings". The second
   half has been on .dpg-section since Step 02 (scroll-margin-top:140px) and the
   reduced-motion blanket below has ALWAYS carried scroll-behavior:auto!important
   to guard this declaration — so the guard predates the thing it guards.
   Added at `Step 10av`, when the Contact hero's jump to #project-form became the
   site's first in-page anchor that actually resolves: dpg/anchor-nav sits on no
   page (handoff §5B/5), so until now there was nothing to scroll to. */
html{scroll-behavior:smooth;}
body{
  font-family:var(--dpg-font-body);
  font-size:var(--dpg-fs-body);
  line-height:var(--dpg-lh-body);
  color:var(--dpg-text);
  background:var(--dpg-white);
  -webkit-font-smoothing:antialiased;
  text-rendering:optimizeLegibility;
  /* RALEWAY'S DEFAULT FIGURES ARE OLD-STYLE — "46,677" and "3 → 26" bob above
     and below the baseline (seen in the first render after the 2026-10-05 font
     switch). Lining figures are set here once and inherited by every element;
     DM Sans is lining already, so only Raleway changes. */
  font-variant-numeric:lining-nums;
}
h1,h2,h3{font-family:var(--dpg-font-heading);font-weight:700;color:var(--dpg-heading);}

/* Visible keyboard focus everywhere (accessibility floor). */
:focus-visible{outline:2px solid var(--dpg-teal);outline-offset:3px;border-radius:var(--dpg-r-xs);}

/* Skip link (paired with <main id="dpg-main">) */
.dpg-skip-link{position:absolute;left:-9999px;top:0;z-index:1000;padding:.75rem 1rem;
  background:var(--dpg-navy);color:var(--dpg-white);font-family:var(--dpg-font-body);font-size:var(--dpg-fs-label);}
.dpg-skip-link:focus{left:.5rem;top:.5rem;}

@media (prefers-reduced-motion:reduce){
  *,*::before,*::after{animation-duration:.001ms!important;transition-duration:.001ms!important;scroll-behavior:auto!important;}
}

/* ----------------------------------------------------------------------------
   4. LAYOUT (container + section rhythm). ONE container, never duplicated.
---------------------------------------------------------------------------- */
.dpg-container{
  width:100%;
  max-width:var(--dpg-container);
  margin-inline:auto;
  padding-inline:var(--dpg-gutter);
}
.dpg-section{padding-block:var(--dpg-section-pad);scroll-margin-top:140px;}
.dpg-section--tint{background:var(--dpg-slate-50);}
.dpg-section--tint-cyan{background:var(--dpg-tint-cyan);}
.dpg-section--dark{background:var(--dpg-navy);color:var(--dpg-text-on-dark-muted);}
/* A SECOND dark ground, deeper and shifted toward teal. --dpg-navy belongs to
   the site's loudest moments — hero, CTA, why-delta — and a problem section is
   a different register: heavier and cooler, not another hero. It is written as
   --dark PLUS --ink rather than a standalone modifier, so every dark-context
   rule already in this file (eyebrow, prose, steps rule, section title, note
   links) applies untouched and --ink overrides nothing but the background.
   Contrast on #061a22: slate-400 6.9:1, cyan 8.7:1, heading-on-dark 12.9:1,
   white 17.8:1 — all pass AA comfortably. */
.dpg-section--dark.dpg-section--ink{background:var(--dpg-ink);}
.dpg-section + .dpg-section{border-top:1px solid var(--dpg-border);}
/* No light divider where a dark band meets a light one, on either side.
   Each exemption needs two classes to outrank the adjacency rule above
   — a bare .dpg-section--dark (0,1,0) loses to .dpg-section + .dpg-section (0,2,0). */
.dpg-section + .dpg-section--dark,
.dpg-section + .dpg-cta,
.dpg-section--dark + .dpg-section,
.dpg-cta + .dpg-section{border-top:0;}

/* ----------------------------------------------------------------------------
   5. COMPONENTS
---------------------------------------------------------------------------- */

/* 5.1 — Grid background utility (client-supplied recipe) -----------------
   Always an overlay layer, so it carries its own positioning - the band it
   sits in paints the background colour and must be position:relative. Retint
   the lines by setting --dpg-grid-line on the band (see 5.17). */
.dpg-grid-bg{
  position:absolute;inset:0;z-index:0;
  background-image:
    linear-gradient(var(--dpg-grid-line) 1px, transparent 1px),
    linear-gradient(90deg, var(--dpg-grid-line) 1px, transparent 1px);
  background-size:64px 64px;
}

/* 5.2 — Badges & eyebrows ----------------------------------------------- */
/* Hero badge (mockup img 4): pill, status dot, mono, UPPERCASE, on dark. */
.dpg-badge{
  display:inline-flex;align-items:center;align-self:flex-start;gap:.55rem;
  padding:.4rem .85rem;
  border:1px solid var(--dpg-border-on-dark);
  border-radius:var(--dpg-r-pill);
  background:rgba(255,255,255,.03);
  font-family:var(--dpg-font-body);
  font-size:var(--dpg-fs-label);
  font-weight:500;
  letter-spacing:.14em;
  text-transform:uppercase;
  color:var(--dpg-slate-400);
}
/* Two card variants of the badge. Both keep the hero badge's mono / 12px /
   uppercase / .14em typography and only restate what differs.

   --lime is the one filled chip on the site: it marks the single pillar that has
   delivered client evidence (build spec §1), so the fill is the point. Navy on
   lime is 13.7:1.

   --plain is a label, not a chip: no fill, no border, NO PADDING, so its text
   starts on the card's content edge and lines up with the heading beneath it.
   It takes the same muted greys the eyebrow uses, and flips on a dark card for
   the same reason the eyebrow does — slate-500 on navy is only 3.66:1, while
   slate-400 is 6.79:1. On white, slate-500 is 4.76:1. */
.dpg-badge--lime{
  border-color:transparent;border-radius:var(--dpg-r-lg);padding:.25rem .75rem;
  background:var(--dpg-lime);color:var(--dpg-navy);
}
.dpg-badge--plain{border:0;background:none;padding:0;color:var(--dpg-slate-500);}
.dpg-card--dark .dpg-badge--plain{color:var(--dpg-slate-400);}
/* The hero's dot is a green "systems online" signal; on a lime chip it is just
   a bullet, so it takes the text colour and drops the glow. */
.dpg-badge--lime .dpg-badge__dot{background:currentColor;box-shadow:none;}

.dpg-badge__dot{
  width:7px;height:7px;border-radius:50%;
  background:var(--dpg-signal);
  box-shadow:0 0 0 3px rgba(95,211,143,.18);
  flex:none;
}

/* Section eyebrow (mockup img 5): accent dash + label, mono, Title Case. */
.dpg-eyebrow{
  display:flex;align-items:center;gap:.7rem;
  margin-bottom:1.1rem;
  font-family:var(--dpg-font-body);
  font-size:var(--dpg-fs-label);
  font-weight:700;
  letter-spacing:.12em;
  color:var(--dpg-slate-500);
}
.dpg-eyebrow::before{
  content:"";width:26px;height:2px;flex:none;
  background:var(--dpg-teal);            /* light-section accent */
}
.dpg-eyebrow__num{color:var(--dpg-heading);}         /* the "02" reads darker */
.dpg-eyebrow--caps{text-transform:uppercase;}
/* Position only. Everything else — the dash, the darker __num, the on-dark
   colours — stays exactly as it is everywhere else, so a centred eyebrow is the
   same object as a left-aligned one rather than a second design. */
.dpg-eyebrow--center{justify-content:center;}
/* On dark bands the eyebrow flips to the cyan accent + light text. The list is
   the set of dark contexts: a --dark section and the hero. Add a new dark band
   here rather than restyling the eyebrow inside it.
   THE CTA BAND IS LIGHT since live's 2026-10 edit (--dpg-slate-200): it keeps
   the cyan dash but takes the light-section text colours. That edit had put
   the dark number on all three contexts, which made every --dark section's
   "0N" navy on navy (axe 1.01:1) — split back out at Step 19f. */
.dpg-section--dark .dpg-eyebrow,
.dpg-hero .dpg-eyebrow{color:var(--dpg-slate-400);}
.dpg-section--dark .dpg-eyebrow::before,
.dpg-hero .dpg-eyebrow::before,
.dpg-cta .dpg-eyebrow::before{background:var(--dpg-cyan);}
.dpg-section--dark .dpg-eyebrow__num,
.dpg-hero .dpg-eyebrow__num{color:var(--dpg-heading-on-dark);}
.dpg-cta .dpg-eyebrow{color:var(--dpg-slate-500);}
.dpg-cta .dpg-eyebrow__num{color:var(--dpg-heading);}
/* THE CYAN BAND IS A THIRD EYEBROW CONTEXT, and this rule fixes a live defect
   rather than enabling a new design (found at Step 10l).

   --dpg-slate-500 is 4.76:1 on white and 4.27:1 on --dpg-tint-cyan. The eyebrow
   is 12px, so the threshold is 4.5:1 and the cyan band FAILS it. That has been
   true on the homepage since Step 12f: dpg/daxbot-proof pairs
   .dpg-section--tint-cyan with a .dpg-eyebrow, so the shipped Daxbot band has
   been carrying a sub-AA label. slate-600 is 6.80:1 on cyan and stays a muted
   label rather than becoming body text.

   Scoped to the band, not applied globally, because slate-500 passes everywhere
   else the eyebrow appears. Added to the context list above rather than
   restyled inside the band, which is the extension point Step 12r established. */
.dpg-section--tint-cyan .dpg-eyebrow{color:var(--dpg-slate-600);}

/* Tag list (Step 16g): a labelled row of short mono chips — the four native
   tools on the Mechanical pillar's §2, pulled out of the paragraph that names
   them so a scanning engineer finds their own platform. A real <ul> under a
   real label (aria-labelledby in the markup). Chips take the client's paler
   lime, --dpg-lime-wash, with slate-800 text at 13.4:1; the label is the
   eyebrow's slate-500, 4.76:1 on white. LIGHT BANDS ONLY — no dark caller
   exists, and a dark one would need the label and rule flipped. */
.dpg-tags{margin-top:2rem;padding-top:1.5rem;border-top:1px solid var(--dpg-border);}
.dpg-tags__label{
  margin-bottom:.9rem;
  font-family:var(--dpg-font-body);font-size:var(--dpg-fs-label);font-weight:500;
  letter-spacing:.12em;text-transform:uppercase;color:var(--dpg-slate-500);
}
.dpg-tags__list{display:flex;flex-wrap:wrap;gap:.5rem;}
.dpg-tags__item{
  padding:.5rem .9rem;border-radius:var(--dpg-r-sm);background:var(--dpg-lime-wash);
  font-family:var(--dpg-font-body);font-size:var(--dpg-fs-label);font-weight:500;
  letter-spacing:.08em;color:var(--dpg-slate-800);
}

/* 5.3 — Buttons --------------------------------------------------------- */
.dpg-btn{
  display:inline-flex;align-items:center;gap:.6rem;
  padding:.85rem 1.4rem;
  border:1px solid transparent;
  border-radius:var(--dpg-r-sm);
  font-family:var(--dpg-font-body);
  font-size:var(--dpg-fs-label);
  font-weight:600;
  letter-spacing:.1em;
  text-transform:uppercase;
  line-height:1;
  transition:background-color var(--dpg-t-fast) var(--dpg-ease), border-color var(--dpg-t-fast) var(--dpg-ease), color var(--dpg-t-fast) var(--dpg-ease), transform var(--dpg-t-fast) var(--dpg-ease);
}
.dpg-btn__arrow{width:.9em;height:.9em;flex:none;}
.dpg-btn:hover{transform:translateY(-1px);}
.dpg-btn:active{transform:translateY(0);}

.dpg-btn--primary{background:var(--dpg-lime);color:var(--dpg-navy);}
.dpg-btn--primary:hover{background:var(--dpg-lime-hover);}

.dpg-btn--ghost{background:transparent;border-color:var(--dpg-border-on-dark);color:var(--dpg-white);}
.dpg-btn--ghost:hover{border-color:var(--dpg-cyan);color:var(--dpg-cyan);}

.dpg-btn--dark{background:var(--dpg-navy);color:var(--dpg-white);}
.dpg-btn--dark:hover{background:var(--dpg-navy-deep);}

/* Ghost variant tuned for LIGHT backgrounds — the light-ground twin of --ghost,
   which is what the homepage hero's "Talk to us" uses on navy.

   ⚠ THE HOVER TEXT WAS --dpg-teal AND FAILED AA. It went unnoticed because this
   variant sat on NO page from Step 09t until Step 10af, so dpg-briefing-notes.md
   §2 carried it under "Dormant — would fail if used". The 404's secondary action
   is the first real caller, which turns a dormant failure into a live one — so it
   is fixed here in the same step rather than shipped.

   Note the split: the BORDER stays --dpg-teal and the TEXT moves to
   --dpg-teal-ink. That is the documented role of the two tokens (field guide
   §6.1) — teal is the brand colour for fills, icons and rules, where the bar is
   3:1 for a non-text UI component and it passes; teal-ink is the darkened cut
   for text on light, where the bar is 4.5:1 and plain teal does not. */
.dpg-btn--outline{background:transparent;border-color:var(--dpg-slate-300);color:var(--dpg-heading);}
.dpg-btn--outline:hover{border-color:var(--dpg-teal);color:var(--dpg-teal-ink);}

/* 5.4 — Header / navbar (mockup img 2): light, sticky, always visible ----
   A sticky element can only travel inside its PARENT's box. A block theme wraps
   every template part in <div class="wp-block-template-part">, and that div is
   exactly as tall as the header inside it — zero scroll range, so the header
   stuck for 0px and scrolled away like a static bar. The wrapper is a plain,
   unstyled, non-semantic div, so display:contents removes its box entirely and
   .dpg-header then sticks inside .wp-site-blocks, which spans the document.
   The tag cannot be suppressed from the template: wp:template-part always
   renders a wrapper, and tagName only changes which element it is. */
.wp-block-template-part{display:contents;}
.dpg-header{
  position:sticky;top:0;z-index:var(--dpg-z-header);
  border-bottom:1px solid var(--dpg-border);
}
/* The frosted bar is painted by this pseudo-element, NOT by .dpg-header itself.
   A non-none backdrop-filter makes an element the containing block for its
   position:fixed descendants. With the blur on .dpg-header, the core Navigation
   block's fixed, inset:0 mobile overlay resolved against the 72px header box
   instead of the viewport, so the open menu was crushed into the hamburger's
   own space. Keeping the blur on a child confines that effect to the child,
   which has no descendants of its own. */
.dpg-header::before{
  content:"";position:absolute;inset:0;z-index:-1;
  background:var(--dpg-navbar-bg);
  -webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);
}
/* Two classes so these win over the core Group block's generated
   .wp-container-* flex layout regardless of stylesheet order. */
.dpg-header .dpg-header__inner{
  display:flex;align-items:center;justify-content:space-between;gap:2rem;
  min-height:72px;
}
.dpg-header .dpg-header__actions{display:flex;align-items:center;gap:2rem;}
.dpg-logo{display:flex;align-items:center;gap:.7rem;}
/* Real logo artwork (client, 2026-10-15). Header: the main horizontal logo,
   trimmed to its ink (6.69:1), at 32px = 214px wide, about the old logo's
   width. Footer: the white stacked logo (2.35:1) at 64px = 150px. Height
   comes from --dpg-logo-h, set per region, so the aspect ratio drives the width.
   The CSS-drawn __mark/__text/__name/__sub lockup that preceded it, dormant
   since Step 12l, was deleted at Step 16f (client). */
.dpg-logo__img{height:var(--dpg-logo-h,50px);width:auto;flex:none;}
.dpg-header{--dpg-logo-h:32px;}
.dpg-footer{--dpg-logo-h:64px;}

/* --- Core Navigation block, skinned to the DPG navbar -------------------
   The header nav is a core wp:navigation block (parts/header.html) so the
   mobile overlay, focus trap and Esc-to-close come from core rather than
   being hand-rolled. We restyle its output; we do not restyle its behaviour. */
.dpg-nav{display:flex;align-items:center;}
.dpg-nav .wp-block-navigation__container{
  display:flex;align-items:center;gap:var(--dpg-nav-gap);
}
/* ⚠ THE COLOUR IS !important AND READS A VARIABLE, and the two go together.
   The !important (from the unrecorded 2026-09-22 work) beats core's
   `.wp-block-navigation-item__content.wp-block-navigation-item__content
   {color:inherit}` (0,3,0) — but it ALSO beat the mobile overlay's white, so
   every overlay link rendered slate-700 on navy, ~1.6:1 (measured, client
   report, 2026-09-29). The overlay now changes the colour by re-declaring
   --dpg-nav-link (5.4, "The open overlay") instead of fighting the !important
   — field guide §3.2. Desktop is unchanged: slate-700 everywhere.
   ⚠ The same !important also silences the teal-ink :hover and current-page
   rules just below on DESKTOP; they have never rendered. KEPT THAT WAY BY
   THE CLIENT'S CHOICE (2026-09-29) — desktop links stay slate on hover; do not
   "fix" this unasked. */
.dpg-nav .wp-block-navigation-item__content{
  --dpg-nav-link:var(--dpg-slate-700);
  font-family:var(--dpg-font-body);font-size:.8125rem;font-weight:500;
  letter-spacing:.1em;text-transform:uppercase;color:var(--dpg-nav-link)!important;
  transition:color var(--dpg-t-fast) var(--dpg-ease),
             background-color var(--dpg-t-fast) var(--dpg-ease);
}
/* --dpg-teal-ink, NOT --dpg-teal. Measured on the frosted bar (#f7f9fb once
   --dpg-navbar-bg composites over white): teal is 3.27:1 and FAILS AA for this
   13px text, teal-ink is 5.26:1 and passes. Fixed at Step 10v while the navbar
   was being reworked; it was two of the four live failures listed in
   dpg-briefing-notes.md §2 and handoff §5A/10 — the hover state and the
   current-page state, both here. */
.dpg-nav .wp-block-navigation-item__content:hover,
.dpg-nav .current-menu-item .wp-block-navigation-item__content{color:var(--dpg-teal-ink);}

/* Hamburger + close buttons */
.dpg-nav .wp-block-navigation__responsive-container-open,
.dpg-nav .wp-block-navigation__responsive-container-close{
  color:var(--dpg-heading);padding:.35rem;
}
.dpg-nav .wp-block-navigation__responsive-container-close{color:var(--dpg-white);}

/* The open overlay: navy panel, light links, gutter-aligned.

   SPECIFICITY WARNING - do not lower these selectors. Core ships
     .wp-block-navigation:not(.has-background)
       .wp-block-navigation__responsive-container.is-menu-open:not(.disable-default-overlay)
       { background-color:#fff }
   plus the matching :not(.has-text-color){color:#000}. That is the default
   whenever the block declares no colours of its own. An earlier (0,3,0) version
   of these rules lost to it: the panel painted white while the link rule below -
   which sits directly on the anchor and did win - kept the text white. The
   result was a full-screen white overlay with invisible text, i.e. the whole
   page appearing to vanish.

   THIS RULE IS NOW THE ONLY THING HOLDING THE OVERLAY NAVY. Until Step 10w
   parts/header.html also set overlayBackgroundColor/overlayTextColor, and those
   emitted .has-*-color classes that WordPress marks !important, so core's
   fallback never even got a chance. They were removed because core propagates
   the same two attributes onto the DESKTOP SUBMENU panels as !important presets,
   which forced the dropdown navy (see 5.4a). The belt is gone; only the braces
   are left.

   So COUNT core's selector properly, because the earlier note here got it
   wrong. :not() contributes its argument's specificity, and core's has two of
   them - :not(.has-background) and :not(.disable-default-overlay) - which makes
   it 5 classes, (0,5,0). The .dpg-header prefix alone is also (0,5,0): a TIE,
   decided only by dpg-global-css loading after core's inlined block styles.
   Measured on the rendered homepage: core's #fff at byte 20431, ours at 48022.
   That is exactly the load-order fragility this file refuses to depend on
   elsewhere, so the redundant :not(.disable-default-overlay) below is carried
   on purpose - it mirrors core's own guard, is always true for this overlay
   (nothing sets that class), and takes the selector to (0,6,0) for a real win. */
/* THE TWO VARIABLES ARE THE REAL LEFT-ALIGNMENT FIX — see the note below.
   They are declared here, on the overlay root, so every core rule that reads
   them re-points at once instead of being fought one selector at a time. */
.dpg-header .dpg-nav.wp-block-navigation .wp-block-navigation__responsive-container.is-menu-open:not(.disable-default-overlay){
  background-color:var(--dpg-navy);
  color:var(--dpg-text-on-dark);
  padding:clamp(1.25rem,4vw,2rem) var(--dpg-gutter);
  --navigation-layout-justification-setting:flex-start;
  --navigation-layout-justify:flex-start;
}
/* Left-align the overlay. The block sets justifyContent:"right" for the
   desktop bar, and core reuses that same value inside the overlay via
   `align-items:var(--navigation-layout-justification-setting)` (resolving to
   flex-end), which pins every link to the right edge. Correct for the bar,
   wrong for a full-screen panel - so it is overridden here only while open.
   Core's rule is (0,3,0) because its :where() adds nothing; these are (0,5,0).

   ⚠ THESE TWO RULES WERE NOT ENOUGH, and the reason is worth keeping. They name
   .__container — but core ALSO right-aligns every <li> individually, at (0,4,0):
     .__responsive-container.is-menu-open:where(…) .__responsive-container-content
       .wp-block-navigation-item{align-items:var(--navigation-layout-justification-setting);
        display:flex;flex-direction:column}
   So each <li> column-stacked its own children and pushed them right. The three
   top-level links only LOOKED correct because their <li> is no wider than its
   text; the Services <li> is as wide as the submenu nested inside it, so its own
   label sat at the far right (client screenshot, assets/img/mobile menu.png).
   Fixed by re-declaring the VARIABLE on the overlay root above rather than by
   adding a third align-items override — field guide §3.2. These two stay as an
   explicit belt for the two containers they already named. */
.dpg-header .dpg-nav.wp-block-navigation .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation__responsive-container-content{
  align-items:flex-start;
  width:100%;
}
/* ⚠ THE VARIABLES MUST BE RE-DECLARED HERE, NOT ONLY ON THE OVERLAY ROOT, and
   this is the whole reason the first attempt left half the menu right-aligned.
   Core copies the nav's own classes onto this <ul>:
     <ul class="wp-block-navigation__container … items-justified-right …
                dpg-nav wp-block-navigation">
   so `.wp-block-navigation.items-justified-right{--navigation-layout-
   justification-setting:flex-end}` matches THIS ELEMENT TOO — and it sits BELOW
   the overlay root, so its own declaration beat the value inherited from above
   for everything inside it. Custom properties inherit, but a descendant that
   re-declares one wins for its whole subtree, and specificity between the two
   declarations never even comes into it: they are on different elements.
   Only the rows carrying .has-child looked right, because the accordion rule
   further down sets their flex-direction and align-items outright; every LEAF
   <li> — the three clusters, Civil, Mechanical — kept core's column + flex-end
   (client screenshot, assets/img/expandable-mobile-menu.png).
   This selector is (0,6,0) against core's (0,2,0) on the same element, so the
   subtree finally reads flex-start. */
.dpg-header .dpg-nav.wp-block-navigation .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation__container{
  flex-direction:column;align-items:flex-start;justify-content:flex-start;
  gap:1.5rem;width:100%;text-align:left;
  --navigation-layout-justification-setting:flex-start;
  --navigation-layout-justify:flex-start;
}
/* The colour goes through --dpg-nav-link because the base link rule's colour
   is !important (see it above) — a plain `color` here renders nothing, which is
   exactly how the overlay's links ended up slate-700 on navy until 2026-09-29.
   Declared on the link itself, (0,6,0), so it also outranks the desktop
   :hover / current-page rules (0,3,0) should those ever start setting the
   variable. White on navy 17:1; cyan on navy ~9:1. */
.dpg-header .dpg-nav.wp-block-navigation .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation-item__content{
  font-size:1.05rem;letter-spacing:.06em;--dpg-nav-link:var(--dpg-text-on-dark);
}
.dpg-header .dpg-nav.wp-block-navigation .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation-item__content:hover{
  --dpg-nav-link:var(--dpg-cyan);
}
.dpg-header .dpg-nav.wp-block-navigation .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation__responsive-container-close{
  color:var(--dpg-text-on-dark);
}

/* Core hides the hamburger above 600px. This design drops the desktop nav at
   900px, so between 600-900 the inline links would overflow the bar. Move
   core's switch to 900px; the .dpg-nav prefix outranks core's own rules. */
@media (min-width:600px) and (max-width:899.98px){
  .dpg-nav .wp-block-navigation__responsive-container:not(.hidden-by-default):not(.is-menu-open){
    display:none;
  }
  .dpg-nav .wp-block-navigation__responsive-container-open:not(.always-shown){
    display:flex;
  }
}

/* 5.4a — Nav submenus: the Services dropdown ---------------------------- */
/* WHAT CORE ALREADY DOES, read off the rendered page rather than recalled
   (field guide §0.1, and the reason A2b/A2c/A2d each cost a debugging round):

   - `Services` is a wp:navigation-submenu WITH a url, so core renders the label
     as a real <a href="/services/"> and a SEPARATE <button class="…submenu__
     toggle"> for the chevron, and marks the item `open-on-hover-click`. That
     gives all three behaviours for free: hover opens on desktop, the chevron
     toggles for touch and keyboard, and the label stays a link to the hub.
     Do NOT set openSubmenusOnClick — it would turn the label into a button and
     the hub would stop being reachable from the nav.
   - Core paints unstyled submenus itself:
     `.wp-block-navigation:not(.has-background) .__submenu-container`
     sets `background:#fff` and `border:1px solid rgba(0,0,0,.15)`, and
     `:not(.has-text-color)` sets `color:#000`. The nav carries neither class, so
     BOTH apply — which means core's own default is already a white panel. The
     rules below keep that background and replace only the border, the radius and
     the type, rather than fighting a colour we are happy with.
   - ⚠ IT WAS NOT ALWAYS THIS WAY. Until Step 10w the nav carried
     overlayBackgroundColor/overlayTextColor, and core propagated those onto the
     submenus as **!important** preset classes, forcing them navy. Removing the
     two attributes is what makes a white panel possible without !important; see
     the note in parts/header.html for why the mobile overlay does not need them.

   Every rule here is (0,4,0) or more, so it beats core's (0,3,0) and (0,4,0)
   outright rather than on source order — `dpg-global-css` happens to load last,
   and relying on that is the fragility field guide §3.5 warns about.

   FIRST USE OF --dpg-shadow-md. All three shadow tokens have been declared and
   unused since Step 02, and build spec §9.4 asks for "soft, low-opacity,
   multi-layer shadows". A panel floating above the page is the one element that
   genuinely needs one. -md rather than -lg: the panel sits just under the bar,
   so it wants to read as lifted, not as hovering far above it. */

/* ONE KNOB FOR THE DROP. The header is min-height:72px with its contents
   centred, so the <li> is only text-height and core opens the panel at
   `top:100%` OF THE ITEM — roughly 26px above the header's bottom edge. This
   variable pushes the panel past that edge and leaves a few pixels of air under
   the bar, and the hover bridge below reads the SAME variable so the two can
   never fall out of sync. Raise it to drop the panel further; the bridge grows
   with it automatically. */
/* THE TWO COLUMN WIDTHS. Separate knobs because the two columns hold different
   material: the pillar names are short ("Civil Engineering"), the cluster names
   are long enough to wrap on purpose ("Workflow Automation & Data Mapping").
   They are also what the panel's total width is made of — see the fit
   calculation above the mega-menu block below before raising either. */
.dpg-header .dpg-nav{
  /* The space between top-level nav items, and the measured width of the
     "HOME" label (Inter 13px caps, .1em tracking = 45px). Together they put
     the dropdown panel at the left edge of "About" from 1440px (client,
     2026-09-29; see 5.4a). Rename or restyle Home and re-measure. */
  --dpg-nav-gap:2rem;
  --dpg-nav-home:2.8125rem;
  --dpg-nav-drop:2.25rem;
  --dpg-nav-col:14rem;
  --dpg-nav-col-sub:19rem;
  /* ONE PANEL ROW, DERIVED NOT GUESSED — and it is load-bearing for the
     panel's HEIGHT the way the two column widths are for its width.
       padding      .55rem + .55rem          = 1.100rem   (the row's own)
       line box     .875rem x 1.8            = 1.575rem   (font-size x line-height)
                                               ---------
                                               2.675rem
     The 1.8 is pinned explicitly on the row itself further down rather than
     left to inherit from body, so this sum cannot drift if body's line-height
     is ever retuned. Change one, change the other. */
  --dpg-nav-row:2.675rem;
}

/* --dpg-r-md, not --dpg-r-lg: the client asked for less rounded corners
   (2026-09-18). One step down the existing radii scale rather than a new value. */
.dpg-header .dpg-nav.wp-block-navigation .wp-block-navigation__submenu-container{
  min-width:var(--dpg-nav-col);
  padding-block:.4rem;
  border:1px solid var(--dpg-border);
  border-radius:var(--dpg-r-md);
  box-shadow:var(--dpg-shadow-md);
  /* Core fades the panel at `opacity .1s linear`, which is fast enough to read
     as a snap. The token is the same curve every other hover on the site uses.
     Only opacity is transitioned — core drives the open/shut with height, width
     and visibility, and animating any of those would fight its state machine. */
  transition:opacity var(--dpg-t-fast) var(--dpg-ease);
}
/* The first-level panel clears the whole bar; nested panels sit flush with their
   parent row, so only the top level takes the drop. */
.dpg-header .dpg-nav.wp-block-navigation .wp-block-navigation__container > .has-child > .wp-block-navigation__submenu-container{
  top:calc(100% + var(--dpg-nav-drop));
}
/* ── ONE BOX, TWO COLUMNS: the second level opens INSIDE the panel ──────────
   Client direction 2026-09-18, given against two screenshots — the shipped
   dropdown marked incorrect, and a reference mega-menu marked wanted. The
   reference devotes its right-hand half to a promo image; the client asked for
   the sub-services to sit there instead, in the SAME box. So hovering a pillar
   no longer opens a second floating panel beside the first: it widens this one
   and the cluster links appear as a ruled right-hand column.

   ⚠ WHAT WAS DELETED HERE, AND WHY IT NEVER ONCE WORKED. Step 10w added a rule
   flipping the nested panel rightward and recorded core's competing rule as
   (0,4,0). Counted off the rendered stylesheet it is (0,6,0):
     .wp-block-navigation.items-justified-right .wp-block-navigation__container
       .has-child .__submenu-container .__submenu-container{left:auto;right:100%}
   Our override was (0,5,0) and LOST. The client's screenshot of the old
   dropdown is the proof: the second panel is still opening leftward, which is
   core winning. That is the THIRD specificity miscount on this one component
   (cf. A2c, and the count corrected in 5.4 above) — all three from recalling a
   core selector instead of counting the one actually served.

   ⚠ AND THE FIRST-LEVEL PANEL HAD TO BE RE-ANCHORED, which is not cosmetic.
   Core pins it to the ITEM'S RIGHT EDGE for a right-justified nav
   (`…items-justified-right … .has-child .__submenu-container{left:auto;right:0}`,
   (0,5,0)). Widening a box pinned on its right edge grows it LEFTWARD, sliding
   the hovered row out from under the pointer — hover drops, the panel closes,
   the box shrinks back under the pointer, and it flickers open/shut forever.
   So the panel is re-anchored left, at (0,6,0), and grows rightward instead.

   IT FITS, AND HERE IS THE ARITHMETIC, because "Services is leftmost so there
   is room" is what the deleted rule assumed and it was never checked. The panel
   starts at the nav's own left edge and is 14rem + 19rem = 528px wide. The nav
   plus its CTA measures ~575px, and both are right-aligned in .dpg-container —
   so the panel ends ~47px short of the container's right edge at 1280px, and
   the margin only widens on larger screens. At the 900px breakpoint the
   container is 852px of content and the panel ends ~47px short there too.
   ⚠ That margin was ~79px until the cluster column went 17rem → 19rem to hold
   "Workflow Automation & Data Mapping" on one line. RE-RUN THIS SUM before
   raising either column again — this is the number that runs out first.
   ⚠ The safe direction is ADDING nav width (Services moves left, more room);
   REMOVING it moves Services right and eats the margin. It is WIDTH that
   matters, not the item COUNT — a shorter label spends the margin exactly as
   deleting an item would.

   ⚠⚠ AND THE MARGIN IS NOW SPENT — `Step 10ay`. "How It Works" was replaced by
   "About" at the client's request: a real page instead of a 404, but ~12
   characters down to ~5. At Inter 13px uppercase with .1em tracking that is
   roughly 55-60px of nav width gone, so the ~47px clearance computes to
   approximately ZERO or a little past it, and the panel now ends at or just
   beyond the container's right edge instead of 47px inside it.
   NOTHING CLIPS: the panel stays well inside the viewport at every width,
   because the container is capped at 1280px and the gutter sits outside it. The
   cost is alignment, not function — the panel's right edge no longer lines up
   inside the content column.
   ⚠⚠ AND THE MARGIN IS BACK — 2026-09-22, and by two separate mechanisms.
   The client reordered the nav to Home / About / Services / Case Study / FAQ.
   (1) "Home" plus its 2rem gap ADDS roughly 65px of nav width, which is the safe
   direction this note has always named, so the nav's left edge moves left by the
   same amount. (2) The panel no longer hangs off the Services item at all — it
   is anchored to the <ul> row (see the block below), so Services sitting third
   costs nothing. Net: nav + CTA is back to roughly 583px against a 528px panel,
   so the clearance is about 55px inside the container — wider than the ~47px of
   `Step 10z` and far better than the ~zero `Step 10ay` left. THE SUM ITSELF IS
   UNCHANGED; only the two inputs moved.

   ⚠ THAT FIGURE IS AN ESTIMATE FROM CHARACTER METRICS, NOT A MEASUREMENT.
   Text width cannot be computed from a stylesheet, so this is the one number on
   the page that a browser has to settle (handoff §5C). Three levers if it reads
   badly, cheapest first: lengthen the nav label ("About Us" buys back ~25px),
   add a nav item (Blog is the obvious candidate and is already in the footer),
   or drop --dpg-nav-col-sub — but that last one is load-bearing for
   "Right-of-Way & Regulatory Mapping" and must not go below ~19rem.

   DESKTOP ONLY. Below 900px core's overlay flattens both levels into a static
   stack (5.4's breakpoint move), where a two-column panel would be nonsense.
   Gating the block is why the overlay unwind further down did not have to grow
   a second time to fight these selectors.

   :has() IS LOAD-BEARING, so the whole thing sits in @supports. Without it a
   browser simply keeps core's flyout — exactly what shipped until today:
   degraded, not broken. The panel cannot instead reserve the right column
   permanently, because a box that is always double width with an empty half
   reads worse than either state.

   NO MARKUP CHANGED. parts/header.html is untouched, so this is still core's
   Navigation block: a real <ul>/<li> tree, a real <a href> on every row
   including the pillar that opens the column, and a separate <button
   aria-expanded> disclosure so touch and keyboard can open it without hover.
   Core hides a closed panel with visibility/height, never display:none, so
   every cluster URL stays in the DOM and crawlable from the header at all
   times. Every selector below is (0,6,0) or more by construction, so each
   clears core's (0,5,0) and (0,6,0) outright rather than by one step. */
@supports selector(:has(*)){
@media (min-width:900px){

  /* ⚠ THE PANEL HANGS OFF THE NAV ROW, NOT OFF THE SERVICES ITEM — 2026-09-22,
     when the client reordered the nav to Home / About / Services / Case Study /
     FAQ and Services stopped being the leftmost item.

     WHY IT HAD TO CHANGE. Core makes every .wp-block-navigation-item
     position:relative, so `left:0` below used to resolve against the Services
     <li>. That was invisible while Services was first, because its left edge WAS
     the nav's left edge — which is exactly what the fit sum above assumes. Put
     two items in front of it and the 528px panel starts ~138px further right
     and ends past the VIEWPORT edge at most desktop widths: a horizontal
     scrollbar, not the cosmetic misalignment `Step 10ay` left behind.

     THE FIX IS THE SAME TECHNIQUE USED ONE LEVEL DOWN, and it is the <ul> that
     is made the containing block rather than the nav or the responsive
     container, because the <ul> is the only ancestor whose box is EXACTLY the
     row of items. That matters for the vertical as much as the horizontal:
     `top` below is `calc(100% + drop)`, a PERCENTAGE OF THE CONTAINING BLOCK'S
     HEIGHT, so anchoring to a taller ancestor would drop the panel further than
     the hover bridge is tall and open a dead strip where hover is lost. The
     <ul> is a flex row whose height is its tallest item, so 100% of it is the
     row's own bottom edge — identical to what the <li> gave.

     ⚠ 2026-09-29 — WHERE THE PANEL STARTS NOW DEPENDS ON THE SCREEN (client).
     Case Study left the nav that day, which moved Home / About / Services
     ~128px RIGHT (the nav is right-aligned, so removing an item after Services
     is the unsafe direction). A same-day version that started the panel midway
     between About and Services then ran 47px off a 1440 screen and 114px off
     1280 and 1024 — measured — so it was withdrawn. What replaced it:
       - from 1440px: the panel starts at the LEFT EDGE OF "ABOUT" — the client's
         choice on their own 1536px viewport. The offset is Home's measured
         width plus one nav gap, `calc(var(--dpg-nav-home) + var(--dpg-nav-gap))`.
         ⚠ --dpg-nav-home is a MEASURED LABEL WIDTH ("HOME", Inter 13px caps,
         .1em tracking = 45px): rename or restyle Home and it must be
         re-measured. The containing block cannot be the About <li> itself,
         because the panel is not its descendant.
       - below 1440px: under Home (left:0), because an About start runs off the
         screen there (16px off at a 1366 window; fits with ~20px to spare at
         1440).
     MEASURED FIT, widest state (a pillar open, 528px), headless Edge,
     `scrollWidth == innerWidth` at every width tested — see registry §9
     `Step 16w` for the table. ⚠ The About start overhangs the "Talk to us"
     button (the panel is wider than the nav plus the button), in the gutter;
     the Home start ends roughly in line with the button.
     ⚠ RE-MEASURE whenever the nav's width changes — adding Case Study back
     moves everything ~128px left and makes room; shortening or removing a
     label does the opposite. */
  .dpg-header .dpg-nav.wp-block-navigation .wp-block-navigation__container{
    position:relative;
  }
  .dpg-header .dpg-nav.wp-block-navigation .wp-block-navigation__container > .has-child{
    position:static;
  }

  /* Grow rightward, not leftward — see the flicker note above. */
  .dpg-header .dpg-nav.wp-block-navigation .wp-block-navigation__container > .has-child > .wp-block-navigation__submenu-container{
    left:0;right:auto;
  }
  @media (min-width:1440px){
    .dpg-header .dpg-nav.wp-block-navigation .wp-block-navigation__container > .has-child > .wp-block-navigation__submenu-container{
      left:calc(var(--dpg-nav-home) + var(--dpg-nav-gap));
    }
  }

  /* The row must stop being the positioning context, or the nested <ul> anchors
     to the ROW instead of to the panel. The panel is position:absolute (core),
     so it becomes the containing block the moment the row goes static. Only the
     PILLAR rows are targeted; the top-level Services <li> stays relative, which
     is what the first panel still hangs off. */
  .dpg-header .dpg-nav.wp-block-navigation .wp-block-navigation__container > .has-child > .wp-block-navigation__submenu-container > .wp-block-navigation-item{
    position:static;
  }

  /* The second level, now a column of the first panel rather than a panel in
     its own right: full height, flush right, one rule dividing it off, and none
     of the border/radius/shadow that made it read as a separate box.
     top+bottom with height:auto is what stretches it — and core's CLOSED state
     still wins, because when top, bottom and height are all non-auto the one
     the spec drops is bottom, leaving height:0 + overflow:hidden intact. */
  .dpg-header .dpg-nav.wp-block-navigation .wp-block-navigation__container > .has-child > .wp-block-navigation__submenu-container > .has-child > .wp-block-navigation__submenu-container{
    top:0;bottom:0;left:auto;right:0;
    width:var(--dpg-nav-col-sub);min-width:0;
    padding-block:.4rem;
    background:transparent;
    border:0;border-left:1px solid var(--dpg-border);
    border-radius:0;
    box-shadow:none;
  }

  /* THE EXPAND ITSELF. The panel reserves the right column only while a pillar
     is open, so it opens one column wide and grows into two. TWO triggers, and
     both are needed: the item is open-on-hover-click, so a pointer opens it via
     :hover while touch and keyboard open it through the toggle button, whose
     aria-expanded is the only thing that changes in the DOM. :has() takes its
     argument's specificity, which is what carries these past core.

     ⚠ aria-expanded IS TRUSTWORTHY HERE AND IS NOT IN THE MOBILE OVERLAY — do
     not "fix" this rule by analogy with the accordion below. Core's getter
     returns `overlayIsOpen || thisSubmenuIsOpen`, so the attribute is pinned
     true whenever the overlay is open. This block is inside
     @media (min-width:900px), where the overlay cannot be open, so the first
     term is always false and the attribute means exactly what it says. */
  /* ⚠ min-width MUST GROW WITH THE PADDING, and leaving it out is what made the
     pillar column collapse into two-line rows the moment the panel expanded
     (client screenshot, 2026-09-18). Two things gang up on it:
       1. box-sizing is border-box, so `min-width:14rem` bounds the WHOLE box —
          padding included. Reserve 19rem of padding-right and the floor stops
          protecting the content column at all.
       2. The panel is position:absolute in a containing block only as wide as
          the word "SERVICES" (~88px), so with nothing holding it open its
          width falls to shrink-to-fit, which floors at min-content — i.e. the
          longest single word. Hence "GIS & Data / Science".
     Adding the two columns together restores the floor: the content box keeps
     its full --dpg-nav-col whatever the padding does. The closed state's
     min-width above is deliberately NOT this value — it is what keeps the panel
     one column wide until a pillar is actually open. */
  .dpg-header .dpg-nav.wp-block-navigation .wp-block-navigation__container > .has-child > .wp-block-navigation__submenu-container:has(> .has-child:hover),
  .dpg-header .dpg-nav.wp-block-navigation .wp-block-navigation__container > .has-child > .wp-block-navigation__submenu-container:has(> .has-child > .wp-block-navigation-submenu__toggle[aria-expanded="true"]){
    padding-right:var(--dpg-nav-col-sub);
    min-width:calc(var(--dpg-nav-col) + var(--dpg-nav-col-sub));
  }

  /* ── AND THE PANEL MUST GROW TALLER TOO, WHICH IT DID NOT UNTIL Step 10ap ──
     ⚠ THIS IS THE HEIGHT TWIN OF THE min-width RULE ABOVE. Read them together.

     THE BUG, reported from a browser (client, 2026-09-21) and the first visual
     defect on this project found by eye rather than by arithmetic: the fourth
     row of the Civil column had no background. It painted only on hover, when
     the row tint fired, and was transparent otherwise.

     WHY. The panel's height comes from the PILLAR column — three rows, one per
     pillar. The cluster column is position:absolute (top:0;bottom:0), so it is
     out of flow and CANNOT push the panel taller, however many rows it holds.
     Three clusters fitted the three-row panel EXACTLY, which is why GIS never
     showed this and why the whole mega-menu looked correct for four build
     steps. The moment Civil took a fourth cluster page its column overflowed
     the panel's white box by exactly one row, and that row sat on the page
     background with nothing behind it.

     THE ARITHMETIC, and it is exact rather than eyeballed:
       one row                          2.675rem   (--dpg-nav-row)
       panel padding-block .4rem x 2    0.800rem
       panel at 3 pillar rows           8.825rem   <- what the box was
       Civil column at 4 rows          11.500rem   <- what it needed
                                       ---------
       deficit                          2.675rem   = precisely one row
     top:0/bottom:0 resolve against the panel's PADDING box, and the column
     carries the same .4rem padding-block, so the two sums use identical terms
     and the fit is exact rather than approximate.

     THE FIX is a min-height rung per row count, applied only while a submenu
     that deep is actually open — so hovering GIS (three rows) still gets a
     three-row panel rather than a four-row one with a blank strip at the
     bottom. box-sizing is border-box, hence the +2px for the panel's own
     1px border top and bottom.

     ⚠ ADD A RUNG WHEN A SUBMENU OUTGROWS SIX. Rungs for 4, 5 and 6 are written
     out because Civil went from one cluster page to four in a single day and is
     the practice most likely to keep growing; six buys real headroom. A submenu
     deeper than the deepest rung does not break loudly — it silently
     reintroduces exactly the bug above. **The tell is a row whose background
     only appears on hover.**

     ⚠ BOTH TRIGGERS, exactly as the min-width rule above needs both: :hover for
     a pointer, and the toggle's aria-expanded for touch and keyboard. The
     toggle and the nested <ul> are SIBLINGS inside the pillar <li> — verified
     against the rendered markup, which emits <a>, then <button
     class="wp-block-navigation-submenu__toggle">, then <ul> — so `~` reaches
     the column from the button. The unequal columns are also why the divider
     border-left on the cluster column now runs past the pillar rows: that is
     correct, and is what makes it read as one two-column box. */
  .dpg-header .dpg-nav.wp-block-navigation .wp-block-navigation__container > .has-child > .wp-block-navigation__submenu-container:has(> .has-child:hover > .wp-block-navigation__submenu-container > .wp-block-navigation-item:nth-child(4)),
  .dpg-header .dpg-nav.wp-block-navigation .wp-block-navigation__container > .has-child > .wp-block-navigation__submenu-container:has(> .has-child > .wp-block-navigation-submenu__toggle[aria-expanded="true"] ~ .wp-block-navigation__submenu-container > .wp-block-navigation-item:nth-child(4)){
    min-height:calc(4 * var(--dpg-nav-row) + .8rem + 2px);
  }
  .dpg-header .dpg-nav.wp-block-navigation .wp-block-navigation__container > .has-child > .wp-block-navigation__submenu-container:has(> .has-child:hover > .wp-block-navigation__submenu-container > .wp-block-navigation-item:nth-child(5)),
  .dpg-header .dpg-nav.wp-block-navigation .wp-block-navigation__container > .has-child > .wp-block-navigation__submenu-container:has(> .has-child > .wp-block-navigation-submenu__toggle[aria-expanded="true"] ~ .wp-block-navigation__submenu-container > .wp-block-navigation-item:nth-child(5)){
    min-height:calc(5 * var(--dpg-nav-row) + .8rem + 2px);
  }
  .dpg-header .dpg-nav.wp-block-navigation .wp-block-navigation__container > .has-child > .wp-block-navigation__submenu-container:has(> .has-child:hover > .wp-block-navigation__submenu-container > .wp-block-navigation-item:nth-child(6)),
  .dpg-header .dpg-nav.wp-block-navigation .wp-block-navigation__container > .has-child > .wp-block-navigation__submenu-container:has(> .has-child > .wp-block-navigation-submenu__toggle[aria-expanded="true"] ~ .wp-block-navigation__submenu-container > .wp-block-navigation-item:nth-child(6)){
    min-height:calc(6 * var(--dpg-nav-row) + .8rem + 2px);
  }
  /* ⚠ RUNGS 7-10 ADDED AT Step 10at, THE STEP THAT NEEDED THE SEVENTH — and
     deliberately four at once rather than one.
     Civil went from ONE cluster page to SEVEN in a single day. Adding a rung
     per page means the fix arrives one step behind the bug every time, and the
     bug is silent: a row with no background that paints only on hover, which no
     curl check, grep or sum in the project documents can see. Ten rows is well
     past any realistic nav depth, so this stops being a recurring trap.
     ⚠ IF A SUBMENU EVER EXCEEDS TEN, add the next rung IN THE SAME STEP as the
     link. The tell is always a row whose background appears only on hover. */
  .dpg-header .dpg-nav.wp-block-navigation .wp-block-navigation__container > .has-child > .wp-block-navigation__submenu-container:has(> .has-child:hover > .wp-block-navigation__submenu-container > .wp-block-navigation-item:nth-child(7)),
  .dpg-header .dpg-nav.wp-block-navigation .wp-block-navigation__container > .has-child > .wp-block-navigation__submenu-container:has(> .has-child > .wp-block-navigation-submenu__toggle[aria-expanded="true"] ~ .wp-block-navigation__submenu-container > .wp-block-navigation-item:nth-child(7)){
    min-height:calc(7 * var(--dpg-nav-row) + .8rem + 2px);
  }
  .dpg-header .dpg-nav.wp-block-navigation .wp-block-navigation__container > .has-child > .wp-block-navigation__submenu-container:has(> .has-child:hover > .wp-block-navigation__submenu-container > .wp-block-navigation-item:nth-child(8)),
  .dpg-header .dpg-nav.wp-block-navigation .wp-block-navigation__container > .has-child > .wp-block-navigation__submenu-container:has(> .has-child > .wp-block-navigation-submenu__toggle[aria-expanded="true"] ~ .wp-block-navigation__submenu-container > .wp-block-navigation-item:nth-child(8)){
    min-height:calc(8 * var(--dpg-nav-row) + .8rem + 2px);
  }
  .dpg-header .dpg-nav.wp-block-navigation .wp-block-navigation__container > .has-child > .wp-block-navigation__submenu-container:has(> .has-child:hover > .wp-block-navigation__submenu-container > .wp-block-navigation-item:nth-child(9)),
  .dpg-header .dpg-nav.wp-block-navigation .wp-block-navigation__container > .has-child > .wp-block-navigation__submenu-container:has(> .has-child > .wp-block-navigation-submenu__toggle[aria-expanded="true"] ~ .wp-block-navigation__submenu-container > .wp-block-navigation-item:nth-child(9)){
    min-height:calc(9 * var(--dpg-nav-row) + .8rem + 2px);
  }
  .dpg-header .dpg-nav.wp-block-navigation .wp-block-navigation__container > .has-child > .wp-block-navigation__submenu-container:has(> .has-child:hover > .wp-block-navigation__submenu-container > .wp-block-navigation-item:nth-child(10)),
  .dpg-header .dpg-nav.wp-block-navigation .wp-block-navigation__container > .has-child > .wp-block-navigation__submenu-container:has(> .has-child > .wp-block-navigation-submenu__toggle[aria-expanded="true"] ~ .wp-block-navigation__submenu-container > .wp-block-navigation-item:nth-child(10)){
    min-height:calc(10 * var(--dpg-nav-row) + .8rem + 2px);
  }

  /* CARET CLOSER TO THE LABEL (client, 2026-09-18). Core gives a submenu link
     flex-grow:1 inside a flex <li>, so the <a> stretched the whole row and shoved
     its sibling toggle <button> against the far edge — which is what put the
     chevron a column away from "GIS & Data Science". Ungrowing the link brings
     the chevron back to the text; trimming the right padding on rows that
     actually HAVE a caret closes the last few pixels. */
  .dpg-header .dpg-nav.wp-block-navigation .wp-block-navigation__submenu-container > .wp-block-navigation-item > .wp-block-navigation-item__content{
    flex-grow:0;
  }
  .dpg-header .dpg-nav.wp-block-navigation .wp-block-navigation__submenu-container > .has-child > .wp-block-navigation-item__content{
    padding-right:.35rem;
  }
  /* The link no longer fills its row, so the hover tint has to move to the row
     or it would stop short of the caret. The link keeps its own identical tint
     as the fallback for a browser that skipped this whole @supports block. */
  .dpg-header .dpg-nav.wp-block-navigation .wp-block-navigation__submenu-container > .wp-block-navigation-item{
    transition:background-color var(--dpg-t-fast) var(--dpg-ease);
  }
  .dpg-header .dpg-nav.wp-block-navigation .wp-block-navigation__submenu-container > .wp-block-navigation-item:hover{
    background:var(--dpg-slate-50);
  }
}
}
/* Submenu items are service NAMES, so they lose the top bar's uppercase and
   tracking — "Workflow Automation & Data Mapping" set in tracked caps is
   unreadable. (0,5,0) to beat `.dpg-nav .wp-block-navigation-item__content`.
   --dpg-heading on white is 17.7:1; the teal-ink hover is 5.55:1. */
/* nowrap — EVERY ROW IN THE PANEL IS ONE LINE (client, 2026-09-18). This was
   `normal` from Step 10v, back when the second level was a narrow flyout and
   wrapping was the only way it fitted. In a mega-menu a wrapped row reads as two
   items, so the columns are sized to the longest label instead and the text is
   forbidden to wrap. **That makes the column widths load-bearing**: if a label
   ever outgrows its column it now OVERFLOWS the panel rather than wrapping
   quietly, which is the failure you want — visible. Fix it by raising
   --dpg-nav-col-sub (or --dpg-nav-col), never by restoring `normal`. The longest
   label today, "Workflow Automation & Data Mapping", is ~247px against the
   19rem column's 267px of text area. Mobile puts `normal` back — see the
   overlay unwind at the end of this section. */
.dpg-header .dpg-nav.wp-block-navigation .wp-block-navigation__submenu-container .wp-block-navigation-item__content{
  padding:.55rem 1.15rem;
  font-size:.875rem;font-weight:500;letter-spacing:0;text-transform:none;
  /* PINNED, NOT INHERITED, AND IT CHANGES NOTHING VISUALLY: 1.8 is exactly what
     this row already computed to, cascading down from body's --dpg-lh-body.
     It is written out because --dpg-nav-row above multiplies by it to size the
     panel, and a height derived from a value that arrives by inheritance is one
     retune of body copy away from being silently wrong. */
  line-height:1.8;
  white-space:nowrap;
  color:var(--dpg-heading);
  /* No transition here on purpose: `.dpg-nav .wp-block-navigation-item__content`
     already carries the colour transition and this is a descendant of it. */
}
.dpg-header .dpg-nav.wp-block-navigation .wp-block-navigation__submenu-container .wp-block-navigation-item__content:hover{
  color:var(--dpg-teal-ink);
  background:var(--dpg-slate-50);
}
/* HOVER BRIDGE, the same technique core uses for its own nested panels. Without
   it the pointer crosses bare header between the label and the dropped panel,
   loses :hover, and the menu snaps shut. Reads --dpg-nav-drop so it always
   covers exactly the distance the panel was pushed down. */
.dpg-header .dpg-nav.wp-block-navigation .wp-block-navigation__container > .has-child > .wp-block-navigation__submenu-container::before{
  content:"";position:absolute;left:0;right:0;bottom:100%;
  height:var(--dpg-nav-drop);
}
/* In the mobile overlay core flattens both levels into the stack, indents them
   and forces `background:transparent!important`. A bordered, rounded, shadowed
   white panel inside that navy stack would read as a stray box, so the panel
   styling is unwound at (0,5,0) — above the rules it is undoing. The drop and
   the sideways offset go too: in the overlay the panels are position:static. */
.dpg-header .dpg-nav.wp-block-navigation .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation__submenu-container{
  min-width:0;padding-block:0;border:0;border-radius:0;box-shadow:none;
  top:auto;left:auto;right:auto;
}
/* white-space back to normal: the desktop panel forbids wrapping because its
   columns are sized to the longest label, but the overlay is a full-width stack
   on a phone where "Workflow Automation & Data Mapping" MUST be allowed to wrap
   or it scrolls the page sideways. A step down in size, because in the accordion
   below these are children of the row above them. */
.dpg-header .dpg-nav.wp-block-navigation .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation__submenu-container .wp-block-navigation-item__content{
  padding:0;background:none;
  white-space:normal;
  font-size:.95rem;letter-spacing:.04em;
}

/* ── MOBILE OVERLAY: the submenus are a COLLAPSED ACCORDION ────────────────
   Client, 2026-09-18, against `assets/img/mobile menu.png`: the sub-services
   should sit UNDER their pillar as a collapsible group, CLOSED until tapped.
   The screenshot showed the whole tree expanded flat — four nav items became
   ten rows, and the material a reader came for was three screens down.

   ⚠ CORE DELIBERATELY FLATTENS SUBMENUS IN THE OVERLAY, so an accordion means
   undoing two of its rules and re-deriving the open state ourselves:
     - it forces every panel open — `height:auto;opacity:1;visibility:visible;
       position:static;overflow:initial` at (0,5,0);
     - it hides the chevrons — `.__submenu-icon{display:none}` at (0,4,0).
   The toggle <button> is in the DOM and fully bound either way; only core's CSS
   says the overlay has no disclosures. Closed rule below is (0,6,0) to clear the
   first; the open rule adds the attribute and lands at (0,8,0).

   ⚠ READ OUT OF core's view.min.js BEFORE BEING RELIED ON, because this is
   behaviour rather than skin (field guide §0.1). For a submenu item
   `state.isSubmenuOpen` resolves to `Object.values(submenuOpenedBy).some(Boolean)`
   and `toggleMenuOnClick` flips `submenuOpenedBy.click`; neither path looks at
   whether the overlay is open. So `aria-expanded` on the toggle is a true
   reflection of panel state and is the correct thing to hang CSS on — no JS of
   ours, and the disclosure semantics stay core's.

   ⚠ THE ONE WRINKLE, and it is core's binding rather than our CSS.
   `openMenuOnHover` bails on `pointerType === "touch"`, so on a real phone only
   the chevron opens a group — correct. With a MOUSE in a narrowed desktop
   window, pointerenter still fires and a group will appear to open on hover.
   Not worth fighting the one component that produced A2, A2b and A2c. Test with
   device emulation, which dispatches touch pointer events.

   NOTE the label is still a real <a href> to the pillar: tapping the text
   navigates, tapping the chevron expands. That separation is the whole reason
   `openSubmenusOnClick` is left off — see parts/header.html. */

/* EVERY ROW STARTS AT THE LEFT — leaves included. The variable re-declared on
   .__container above is the real fix and covers this; this rule is the belt,
   because the variable's correctness depends on no OTHER element re-declaring
   it further down, and core has already been caught doing exactly that once.
   (0,5,0) against core's (0,4,0); the .has-child rule below is (0,6,0) and
   still wins where it needs to. */
.dpg-header .dpg-nav.wp-block-navigation .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation-item{
  align-items:flex-start;
}

/* Label and chevron share a row; the submenu wraps onto its own line beneath.
   Core column-stacks this <li> at (0,4,0); this is (0,6,0). */
.dpg-header .dpg-nav.wp-block-navigation .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation-item.has-child{
  flex-direction:row;flex-wrap:wrap;align-items:center;
  width:100%;
  column-gap:.25rem;row-gap:0;
}
/* Keeps the chevron beside its label at BOTH levels. Without this the two rows
   disagree: core gives `flex-grow:1` only to links INSIDE a submenu container
   (0,5,0), so Services' button sat next to its label while GIS's was pushed to
   the far edge. To move both to the right edge instead, this becomes
   `margin-left:auto` on .dpg-nav__disclosure — one declaration. */
.dpg-header .dpg-nav.wp-block-navigation .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation-item.has-child > .wp-block-navigation-item__content{
  flex-grow:0;
}

/* OUR OWN DISCLOSURE BUTTON, injected by assets/js/site.js. Hidden everywhere
   except the open overlay, so the desktop panel keeps core's chevron and its
   hover behaviour untouched. 44px because core's own is `.6em` (~10px), under
   the WCAG 2.2 24x24 target minimum; the glyph is sized on the <svg>. */
.dpg-nav__disclosure{display:none;}
.dpg-header .dpg-nav.wp-block-navigation .wp-block-navigation__responsive-container.is-menu-open .dpg-nav__disclosure{
  display:flex;align-items:center;justify-content:center;
  width:2.75rem;height:2.75rem;margin:0;padding:0;
  background:none;border:0;color:inherit;cursor:pointer;
}
.dpg-header .dpg-nav.wp-block-navigation .wp-block-navigation__responsive-container.is-menu-open .dpg-nav__disclosure svg{
  width:.8rem;height:.8rem;
  transition:transform var(--dpg-t-fast) var(--dpg-ease);
}
/* Down when shut, up when open — and this attribute is OURS, so it is reliable.
   Core's identically-named attribute is not; see the note above the rules below. */
.dpg-header .dpg-nav.wp-block-navigation .wp-block-navigation__responsive-container.is-menu-open .dpg-nav__disclosure[aria-expanded="true"] svg{
  transform:rotate(180deg);
}
/* ⚠ WHY THE OPEN STATE IS A CLASS AND NOT core's aria-expanded, which is what
   the first build of this accordion used and why it never collapsed.
   In the overlay core pins that attribute true on EVERY toggle. From
   view.min.js:
     get isSubmenuOpen(){ let e=o();
       return Object.values(e.overlayOpenedBy||{}).filter(Boolean).length>0
           || l.isMenuOpen }
   Interactivity API contexts INHERIT, so inside a submenu <li> the <nav>'s
   overlayOpenedBy is in scope — and while the overlay is open one of its values
   is true, so the getter short-circuits to true everywhere. That is correct for
   core's own flattened overlay; it just makes the attribute useless to us. And
   it cannot be written over either: data-wp-bind--aria-expanded is reactive, so
   any value we set is clobbered on the next state change. Hence our own button
   with our own attribute, and .dpg-submenu-open as the state.

   GATED ON .dpg-nav--accordion, which site.js adds only after the buttons are
   in. No JS, no collapse — the overlay stays exactly as core renders it, every
   group expanded and every link reachable. Degraded, never broken. */
.dpg-header .dpg-nav--accordion .wp-block-navigation__responsive-container.is-menu-open .has-child .wp-block-navigation__submenu-container{
  display:none;
}
/* OPEN. Indented under its parent with a hairline to carry the nesting, which
   compounds correctly at the second level because both levels match this rule.
   Overrides core's `padding-left:2rem;padding-right:2rem` (0,5,0). */
/* ⚠ THE SPACING ABOVE THE GROUP IS padding-top, NOT margin-top, and that is
   what makes the animation smooth. site.js animates `height` between 0 and
   scrollHeight, and scrollHeight includes padding but not margin — so a
   margin-top would jump its full 0.85rem on the first frame while the rest of
   the panel slid open. Keep the gap inside the box.
   `overflow:hidden` is what lets a short height crop the content mid-slide. */
.dpg-header .dpg-nav--accordion .wp-block-navigation__responsive-container.is-menu-open .has-child.dpg-submenu-open > .wp-block-navigation__submenu-container{
  display:flex;flex-direction:column;
  flex-basis:100%;width:100%;
  gap:.85rem;
  margin:0;
  padding:.85rem 0 0 1.1rem;
  border-left:1px solid var(--dpg-border-on-dark);
  overflow:hidden;
  transition:height var(--dpg-t-med) var(--dpg-ease);
}

.dpg-header .dpg-btn--dark{padding:.7rem 1.15rem;}

/* 5.5 — Hero (mockup img 1 + img 4) ------------------------------------- */
.dpg-hero{position:relative;overflow:hidden;background:var(--dpg-navy);color:var(--dpg-text-on-dark);}
/* .dpg-hero__grid carries no rule of its own - .dpg-grid-bg positions itself. */
/* The <picture> that supplies the AVIF/WebP ladder must not become a layout box
   of its own: display:contents removes it entirely and leaves .dpg-hero__media
   positioning against .dpg-hero, exactly as it did when the <img> was a direct
   child. Without this the wrapper is an inline box in the hero's flow. */
.dpg-hero picture{display:contents;}
.dpg-hero__media{
  position:absolute;top:0;right:0;bottom:0;z-index:1;
  /* Anchored to the CONTENT COLUMN, not the viewport: the photo always begins
     --dpg-hero-media-start to the right of the container's left edge, so its
     distance from the headline is identical at every width while it still
     bleeds to the right screen edge. A flat width percentage cannot do this -
     the container is centred and capped at 1280px, but a % tracks the viewport,
     so the two drift apart and the photo detaches on wide screens.

     The width must be EXPLICIT. For an absolutely positioned replaced element,
     width:auto resolves to the image's intrinsic width (1672px) rather than
     stretching between left and right, which overflows the section. The
     percentages below resolve against .dpg-hero, i.e. the true content width -
     100vw would wrongly include the scrollbar. */
  filter: brightness(0.5)!important;
  right:0;left:auto;
  /* width:calc(100% - max(0px, (100% - var(--dpg-container)) / 2) - var(--dpg-hero-media-start)); */
  height: 82vh;
  width:calc(100% - max(0px, (100% - var(--dpg-container)) / 2) - 50px);
  max-width:none;
  object-fit:cover;object-position:left center;
  /* Fade length is fixed, not a percentage: a % fade grows as the photo grows,
     so the opaque edge would creep toward the headline on large screens. */
  -webkit-mask-image:linear-gradient(90deg,transparent 0,#000 var(--dpg-hero-media-fade));
          mask-image:linear-gradient(90deg,transparent 0,#000 var(--dpg-hero-media-fade));
  pointer-events:none;
}
/* --right (client, 2026-09-30, `Step 16bh`): anchor the crop to the photo's
   RIGHT edge instead of its left, for a hero whose subject sits at the right
   and would otherwise be cut when the slot is taller than the file is wide — the
   services hub's ~2.33:1 photo lost its GIS screen at 1920×1080. Opt-in per
   hero via dpg_picture()'s 'class'; every other hero keeps left center. Placed
   straight after the base rule so it wins at equal specificity. */
.dpg-hero__media--right{object-position:right center;}
.dpg-hero__inner{position:relative;z-index:2;
  padding-block:clamp(3.5rem,7vw,6rem);
  display:flex;flex-direction:column;min-height:min(84vh,760px);}
.dpg-hero__content{flex:1 1 auto;display:flex;flex-direction:column;justify-content:center;max-width:640px;}
.dpg-hero__title{
  font-size:var(--dpg-fs-h1);font-weight:700;line-height:var(--dpg-lh-tight);
  letter-spacing:-0.04em;color:var(--dpg-text-on-dark);
  max-width:780px;margin-block:1.5rem 0;
}
.dpg-hero__title .accent{color:var(--dpg-cyan);}
.dpg-hero__lead{
  font-size:var(--dpg-fs-lead);line-height:var(--dpg-lh-lead);
  color:var(--dpg-hero-lead,var(--dpg-text-on-dark-muted));max-width:620px;margin-block:1.4rem 2.5rem;
}
.dpg-hero__actions{display:flex;flex-wrap:wrap;gap:1rem;}

/* Hero meta strip (mockup img 1 footer row).
   slate-400, not slate-500 (client, 2026-09-30, `Step 16ap`): slate-500 was
   3.66:1 on plain navy — below AA for 12px text on every hero. slate-400 is
   6.79:1 on navy and measured >=4.4:1 at the worst pixel over the photo on eight
   heroes at 1536 and 1280. The one exception is the homepage's middle label over
   the collage's photo panel (handoff §5A/5). */
.dpg-hero__meta{
  position:relative;z-index:2;
  display:flex;justify-content:space-between;gap:1rem;flex-wrap:wrap;
  padding-top:1.25rem;flex:none;
  border-top:1px solid var(--dpg-border-on-dark);
  font-family:var(--dpg-font-body);font-size:var(--dpg-fs-label);letter-spacing:.14em;
  text-transform:uppercase;color:var(--dpg-slate-400);
}

/* Pillar hero (--pillar). The same band, the same photo treatment, the same
   content width as the homepage hero — every pillar page shares this variant
   and differs only in copy, and later in accent via the dpg-page--{slug} body
   class (build spec §9.7, "accented-same").

   It overrides ONE thing, because that is all that actually differs: §S1 runs
   to two lead paragraphs where the homepage has one. The pair reads as a single
   block, so each lead carries only a top margin and the space before the CTA
   moves to .dpg-hero__actions. That works for one paragraph or four without a
   positional selector — and avoids a :last-of-type, which would have silently
   counted the eyebrow, since .dpg-eyebrow is also a <p>.

   The content column is deliberately NOT widened. The photo fades in over
   --dpg-hero-media-fade and is fully opaque from roughly 593px, so the
   inherited 640px cap is what keeps the text clear of it. */
/* --compact: a SHORTER hero band, for the blog index (client, 2026-09-22).
   The brief is that a slice of the content below must be visible once the page
   has loaded, so the band cannot own the viewport the way a pillar hero does.

   The only knob is .dpg-hero__inner's min-height, which is min(84vh,760px) on
   the base hero — that 84vh is what makes every other hero fill the screen. At
   min(52vh,440px) the band plus the 72px sticky header comes to roughly 500px,
   so on a ~800px viewport about 300px of what follows is already on screen.
   Padding steps down with it, or the content would sit hard against the edges of
   a band half the height it was written for. */
.dpg-hero--compact .dpg-hero__inner{
  min-height:min(52vh,440px);
  padding-block:clamp(2.5rem,5vw,3.75rem);
}

.dpg-hero--pillar .dpg-hero__lead{margin-block:1.4rem 0;}
.dpg-hero--pillar .dpg-hero__lead + .dpg-hero__lead{margin-block:1.1rem 0;}
.dpg-hero--pillar .dpg-hero__actions{margin-top:2.5rem;}

/* STRONGER HERO FADES — THE THREE PRACTICES' PAGES (client: Civil and Mechanical
   2026-09-29; GIS added 2026-09-30, `Step 16ba`, when the GIS Workflow photo put
   a pale map screen behind the lead at 1.7-1.8:1 on the base hero).
   Their own photographs (Steel, Product Design…) are brighter and busier than
   the dark stand-ins, and the base hero lets the photo run behind the lead and
   the meta strip: measured on Product Design, the lead was 2.2:1 and the meta
   labels 2.4-3.4:1 over the dimmed photo. On these pages the photo's mask gets
   two gradients instead of one, INTERSECTED, so the photo is visible only where
   both allow it:
     1. LEFT — fully transparent for --dpg-hero-fade-clear, then fading in over
        --dpg-hero-fade-length, so the navy runs further under the text column;
     2. BOTTOM — fully transparent for --dpg-hero-floor-clear above the photo's
        foot, then fading in over --dpg-hero-floor-length, so the CTA row and
        the meta strip sit on the band's own navy.
   Masking (rather than a navy overlay) lets the band's grid texture show
   through where the photo is faded, exactly as it does on the left today.
   Scoped by the dpg-practice--{civil|mechanical|gis} body class (inc/setup.php),
   which every page under those pillars inherits — PLUS THE SERVICES HUB by its
   own body class, dpg-page--services (client, `Step 16bh`: its wide photo, crop
   anchored right to keep all three screens, put bright office behind the text —
   labels 2.2:1 at 1920 without these fades). NOT the homepage, About or Contact
   (client). GIS was left out at first (2026-09-29) and joined at `Step 16ba`. DESKTOP ONLY: from 1300px down the photo
   is already a 28%-opacity backdrop with its own vertical mask (§7).
   mask-composite:intersect is the standard; -webkit-mask-composite:source-in is
   the older WebKit spelling of the same thing.
   ⚠ THE PHOTO IS STRETCHED TO THE BAND'S HEIGHT HERE (height:100% of .dpg-hero,
   its containing block), replacing the base 82vh. NOT height:auto — on an
   absolutely positioned REPLACED element auto resolves to the image's own
   height, exactly the width trap noted on the base rule (tried, measured). At 82vh the photo is
   TALLER than the band on a tall screen (1920×1080: 886px against ~760px) and
   its bottom edge — and so this bottom fade — fell below the band, out of
   sight, leaving the meta strip over the photo (measured). Band-height makes
   the fade's distances count from the band's own foot at every screen size:
   the meta strip's rule sits ~9rem above it (the band's 6rem bottom padding
   plus the strip), so the photo is clear to 8.5rem and fades in over 9rem. */
@media (min-width:1301px){
  .dpg-practice--civil .dpg-hero,
  .dpg-practice--mechanical .dpg-hero,
  .dpg-practice--gis .dpg-hero,
  .dpg-page--services .dpg-hero{
    --dpg-hero-fade-clear:24rem;
    --dpg-hero-fade-length:26rem;
    --dpg-hero-floor-clear:8.5rem;
    --dpg-hero-floor-length:9rem;
  }
  .dpg-practice--civil .dpg-hero__media,
  .dpg-practice--mechanical .dpg-hero__media,
  .dpg-practice--gis .dpg-hero__media,
  .dpg-page--services .dpg-hero__media{
    height:100%;
    -webkit-mask-image:
      linear-gradient(90deg,transparent 0,transparent var(--dpg-hero-fade-clear),#000 calc(var(--dpg-hero-fade-clear) + var(--dpg-hero-fade-length))),
      linear-gradient(0deg,transparent 0,transparent var(--dpg-hero-floor-clear),#000 calc(var(--dpg-hero-floor-clear) + var(--dpg-hero-floor-length)));
            mask-image:
      linear-gradient(90deg,transparent 0,transparent var(--dpg-hero-fade-clear),#000 calc(var(--dpg-hero-fade-clear) + var(--dpg-hero-fade-length))),
      linear-gradient(0deg,transparent 0,transparent var(--dpg-hero-floor-clear),#000 calc(var(--dpg-hero-floor-clear) + var(--dpg-hero-floor-length)));
    -webkit-mask-composite:source-in;
            mask-composite:intersect;
  }
}

/* LIGHTER HERO LEAD — THE THREE PRACTICES' PAGES (client, 2026-09-30: Civil and
   Mechanical at `Step 16an`, GIS joined at `Step 16ba` with the fades above). The fades above keep most of a photo out from under the text,
   but not all of it: on 3D Modeling the lead's line ends ran over a pale monitor
   and measured 3.8-4.1:1 in the brightest 5% (slate-400; AA needs 4.5). slate-300
   measured 5.5:1 at its WORST pixel there, so the next bright photo is covered
   too. Re-declares the lead's colour variable rather than out-selecting it;
   every width, since below 1301px the photo sits behind the whole lead. */
.dpg-practice--civil .dpg-hero,
.dpg-practice--mechanical .dpg-hero,
.dpg-practice--gis .dpg-hero,
.dpg-page--services .dpg-hero{--dpg-hero-lead:var(--dpg-slate-300);}


/* 5.6 — Section heading + lead (rest of page, light bg) ----------------- */
.dpg-section__head{max-width:820px;}
/* A section head WITH AN ACTION beside it — heading left, one button right,
   bottoms aligned so the button sits on the heading's last line; wraps under
   the heading when there is no room. Added Step 15i for the homepage's Latest
   insights ("View all articles"); generic, so any section with a "see all" link
   can use it. It owns the gap to the content below, which .dpg-card-grid--posts
   deliberately does not have. */
.dpg-section__head-row{
  display:flex;flex-wrap:wrap;align-items:flex-end;justify-content:space-between;
  gap:1.5rem 2rem;margin-bottom:2.75rem;
}
.dpg-section__title{
  font-family:var(--dpg-font-heading);font-size:var(--dpg-fs-h2);font-weight:700;
  line-height:var(--dpg-lh-heading);letter-spacing:-0.03em;color:var(--dpg-heading);
}
.dpg-section__title .accent{color:var(--dpg-teal-ink);}
/* Statement heading: still an <h2> (one <h1> per page), but rendered at the H1
   step of the existing type scale, with the same tighter tracking the H1 uses.
   No new size token - nothing about the scale changes, only which step this
   heading sits on (mockup section-2-design.png). */
.dpg-section__title--display{font-size:var(--dpg-fs-h1);letter-spacing:-0.04em;}
.dpg-section--dark .dpg-section__title{color:var(--dpg-heading-on-dark);}
.dpg-section--dark .dpg-section__title .accent{color:var(--dpg-cyan);}
/* .dpg-prose is used two ways: as a WRAPPER around paragraphs, and directly on
   a single paragraph (four seeded patterns do the latter for their closing
   note). Both forms are styled. On a light band they are indistinguishable —
   body already carries this size, line-height and colour — but only the
   descendant form would pick up the dark-band flip below, so the second form
   would silently render navy text on navy. Extending the two selector lists
   costs nothing and removes the trap permanently. */
.dpg-prose p,
p.dpg-prose{font-size:var(--dpg-fs-body);line-height:var(--dpg-lh-body);color:var(--dpg-text);}
.dpg-prose p + p{margin-top:1.2rem;}
.dpg-section--dark .dpg-prose p,
.dpg-section--dark p.dpg-prose{color:var(--dpg-slate-400);}
.dpg-section__head .dpg-prose{margin-top:1.4rem;}
/* A bolded clause inside prose must lift off the muted body colour or the
   emphasis is invisible on a dark band. */
.dpg-section--dark .dpg-prose strong{color:var(--dpg-lime);}
.dpg-section__note{margin-top:2.5rem;max-width:640px;}
/* --statement: the closing line of a section that argues to a conclusion. Set
   at the lead step and, on dark, at full brightness rather than the muted body
   colour, so the resolution reads as the payoff it is. Deliberately NOT paired
   with .dpg-prose in markup: .dpg-section--dark p.dpg-prose is also (0,2,0) and
   would then be decided by source order. */
.dpg-section__note--statement{
  max-width:820px;font-size:var(--dpg-fs-lead);line-height:var(--dpg-lh-lead);
  color:var(--dpg-heading);
}
.dpg-section--dark .dpg-section__note--statement{color:var(--dpg-text-on-dark);}
.dpg-section__note--center{max-width:none;text-align:center;}
/* Base `a` is color:inherit, so an inline link needs its colour stated or it
   is only distinguishable by the underline. --dpg-teal-ink, not --dpg-teal:
   the brand teal is 3.45:1 on white and fails AA as text, which is the whole
   reason the -ink token exists (field guide §6.1). Cyan is the on-dark
   counterpart at 8.5:1 on navy. */
.dpg-section__note a{color:var(--dpg-teal-ink);text-decoration:underline;text-underline-offset:.2em;transition:color var(--dpg-t-fast) var(--dpg-ease);}
.dpg-section__note a:hover{color:var(--dpg-teal);}
.dpg-section--dark .dpg-section__note a,
.dpg-cta .dpg-section__note a{color:var(--dpg-cyan);}

/* 5.7 — CTA band (dark, centered) --------------------------------------- */
.dpg-cta{background:var(--dpg-slate-200);color:var(--dpg-text-on-dark);text-align:center;}
/* The band's vertical rhythm comes from __inner (--dpg-cta-pad, 96px). Zero the
   .dpg-section padding it also carries so the two don't stack to 176px.
   Two classes, so this outranks the .dpg-section rule in §4. */
.dpg-section.dpg-cta{padding-block:0;}
.dpg-cta__inner{padding-block:var(--dpg-cta-pad);max-width:720px;margin-inline:auto;}
.dpg-cta__title{font-family:var(--dpg-font-heading);font-size:var(--dpg-fs-h2);font-weight:700;
  line-height:var(--dpg-lh-heading);letter-spacing:-0.03em;color:var(--dpg-heading);}
/* The band's heading sits on the same display step as every section heading on
   the page. It cannot simply borrow .dpg-section__title--display: both are
   (0,1,0) and .dpg-cta__title is written later, so it would win on source order.
   Same two tokens, applied where the cascade can reach. */
.dpg-cta__title--display{font-size:var(--dpg-fs-h1);letter-spacing:-0.04em;}
.dpg-cta__title .accent{color:var(--dpg-teal-ink);}
.dpg-cta__lead{font-size:var(--dpg-fs-lead);line-height:var(--dpg-lh-lead);
  color:var(--dpg-slate-600);margin-top:1rem;}
.dpg-cta__actions{margin-top:2rem;display:flex;gap:1rem;justify-content:center;flex-wrap:wrap;}
/* --grid carries the hero's engineering texture. It clips and contains its own
   .dpg-grid-bg layer and lifts the content above it. Kept local rather than
   shared with .dpg-case's identical pair: the two lift different inner
   elements, and coupling unrelated components in one selector list trades a
   duplicated declaration for a worse maintenance trap. */
.dpg-cta--grid{position:relative;overflow:hidden;}
.dpg-cta--grid > .dpg-cta__inner{position:relative;z-index:1;}

/* 5.8 — Footer (global, dark) -------------------------------------------- */
.dpg-footer{background:var(--dpg-navy-deep);color:var(--dpg-slate-400);}
.dpg-footer__inner{padding-block:var(--dpg-section-pad) 2rem;}
.dpg-footer__top{
  display:flex;flex-wrap:wrap;gap:3rem;justify-content:space-between;
  padding-bottom:var(--dpg-section-pad);
}
.dpg-footer__brand{max-width:320px;}
.dpg-footer__tagline{margin-top:1.1rem;font-size:var(--dpg-fs-small);line-height:var(--dpg-lh-lead);color:var(--dpg-slate-400);}
.dpg-footer__nav{display:flex;flex-wrap:wrap;gap:3rem;}
.dpg-footer__col{display:flex;flex-direction:column;gap:.85rem;}
/* The links are a real <ul> so a screen reader announces "list, 4 items" — with
   four columns and thirteen links that grouping is doing work. Spacing is
   unchanged from when they were bare <a>s: the column's own .85rem gap now falls
   between the heading and this list, which with the heading's .3rem margin is
   the same 1.15rem the first link always had. */
.dpg-footer__list{
  display:flex;flex-direction:column;gap:.85rem;
  margin:0;padding:0;list-style:none;
}
/* ⚠ AA FIX (Step 15q). The column headings and the copyright line were
   --dpg-slate-500: 3.75:1 on --dpg-navy-deep, under the 4.5:1 that 12px text
   needs — PageSpeed's accessibility audit flagged all five (axe-core
   color-contrast, the engine Lighthouse runs). Both are now --dpg-slate-400,
   6.96:1, the colour the links beside them already use; no token sits between
   the two, and the headings still read as labels by their mono / uppercase /
   tracked type rather than by being dimmer. Footer-only classes, so nothing
   else on the site moves. */
.dpg-footer__heading{
  font-family:var(--dpg-font-body);font-size:var(--dpg-fs-label);font-weight:500;
  letter-spacing:.12em;text-transform:uppercase;color:var(--dpg-slate-400);margin-bottom:.3rem;
}
.dpg-footer__link{font-size:var(--dpg-fs-small);color:var(--dpg-slate-400);transition:color var(--dpg-t-fast) var(--dpg-ease);}
.dpg-footer__link:hover{color:var(--dpg-cyan);}
.dpg-footer__bottom{
  padding-top:2rem;border-top:1px solid var(--dpg-border-on-dark);
  font-family:var(--dpg-font-body);font-size:var(--dpg-fs-label);letter-spacing:.06em;
  color:var(--dpg-slate-400);
  /* Step 19a: the copyright left, the legal links right; stacked when they
     no longer fit side by side. */
  display:flex;flex-wrap:wrap;gap:1rem 2rem;justify-content:space-between;align-items:baseline;
}
/* The legal links take the bar's mono label type rather than the columns'
   body-size links — they belong to the copyright line, not the sitemap. The
   colour and cyan hover still come from .dpg-footer__link. */
.dpg-footer__legal{display:flex;flex-wrap:wrap;gap:.5rem 1.5rem;margin:0;padding:0;list-style:none;}
.dpg-footer__legal .dpg-footer__link{font-size:inherit;}

/* 5.9 — Proof strip (shared metrics, [dpg_proof_strip]) ------------------
   A compact white band, not a full section: 35px of vertical padding rather
   than the 80px --dpg-section-pad it would otherwise inherit. Horizontal inset
   comes from .dpg-container, NOT the reference's own clamp(), so the figures
   line up with every other section's text (build spec §7.1, §9.1). */
.dpg-section.dpg-proof-strip{
  padding-block:35px;
  background:var(--dpg-white);
  border-bottom:1px solid var(--dpg-border);
}
/* The strip draws its own bottom rule, so the next section must not add a top
   one as well - that would render as a 2px double line. */
.dpg-proof-strip + .dpg-section{border-top:0;}

.dpg-proof-strip__grid{
  display:grid;grid-template-columns:repeat(4,1fr);
}
/* Dividers are borders on the columns themselves, so they stretch the full
   cell height and disappear correctly when the grid reflows. */
.dpg-proof-strip__stat{
  padding-inline:clamp(1.25rem,3vw,3rem);
  border-left:1px solid var(--dpg-border);
}
.dpg-proof-strip__stat:first-child{border-left:0;padding-inline-start:0;}

.dpg-proof-strip__figure{
  font-family:var(--dpg-font-heading);
  font-size:var(--dpg-fs-stat);
  font-weight:700;
  line-height:var(--dpg-lh-tight);
  letter-spacing:-0.03em;
  color:var(--dpg-heading);
  margin-bottom:.375rem;
}
/* The +, % and → are the accent, set smaller than the numerals. em-based so
   they track --dpg-fs-stat at every breakpoint. */
.dpg-proof-strip__accent{
  color:var(--dpg-teal);
  font-size:.6em;
  letter-spacing:0;
}
.dpg-proof-strip__accent--arrow{font-size:.72em;margin-inline:.18em;}

/* Label is deliberately the existing mono/uppercase/slate treatment, unchanged
   - it already matches the design reference, so no new class is introduced. */
.dpg-proof-strip__label{
  font-family:var(--dpg-font-body);font-size:var(--dpg-fs-label);font-weight:500;
  letter-spacing:.08em;text-transform:uppercase;color:var(--dpg-slate-500);
}

/* --ledger: THE SAME metrics component turned on its side. Under the homepage
   hero it is four equal columns of equal figures; here it is a stack of ruled
   rows, label leading and value answering. Two reasons it is a modifier and
   not a second component: it reuses __figure, __label and __accent untouched
   (including __accent--arrow's visually-hidden " to ", which is what lets a
   BEFORE -> AFTER value be announced correctly), and it changes only axis,
   label position and scale.

   Rules are border-TOP and inline padding is zero, deliberately: the base
   component's responsive rules zero border-left and inline padding at 900px
   and 560px, so those rules agree with this layout instead of fighting it.
   Written the other way round the ledger would have needed to out-specify a
   media query — the trap in field guide §3.1. */
.dpg-proof-strip--ledger .dpg-proof-strip__grid{display:block;}
.dpg-proof-strip--ledger .dpg-proof-strip__stat{
  display:flex;flex-wrap:wrap;align-items:baseline;justify-content:space-between;gap:.35rem 1.5rem;
  padding-block:1.1rem;padding-inline:0;
  border-left:0;border-top:1px solid var(--dpg-border);
}
.dpg-proof-strip--ledger .dpg-proof-strip__stat:last-child{border-bottom:1px solid var(--dpg-border);}
/* In a row the figure answers the label rather than heading it, so it loses the
   stacked margin and drops to the H3 step. The featured figure beside it is the
   one that should dominate; four more 44px numerals would flatten the set. */
.dpg-proof-strip--ledger .dpg-proof-strip__figure{
  margin-bottom:0;font-size:var(--dpg-fs-h3);flex:none;text-align:right;
}

/* The flagship figure owns its own column at the display step. 46,677 is oddly
   specific, and that specificity IS the argument (build spec §1: make the
   evidence impossible to misunderstand) — so it is sized to be read as a fact
   rather than summarised into a headline. Second home for --dpg-fs-stat-xl,
   after .dpg-case__metric--featured. */
.dpg-proof-strip__figure--feature{
  font-size:var(--dpg-fs-stat-xl);letter-spacing:-0.04em;margin-block:.25rem 0;
}

/* A section whose head sits ABOVE a split rather than inside one column of it.
   Components that follow .dpg-section__head carry their own top margin by
   convention (see .dpg-audience, .dpg-steps); .dpg-split is used both ways, so
   the margin belongs on the adjacency rather than on the component. */
.dpg-section__head + .dpg-split{margin-top:2.75rem;}

/* Breadcrumbs (inc/breadcrumbs.php). Currently rendered inside .dpg-hero, so
   the colours below are the on-dark set; a light-band placement would
   re-declare --dpg-crumb-ink the way .dpg-steps re-declares its rule colour.

   The separator is ::before content on the <li> rather than a character in the
   markup, so it is never announced — a "/" between every crumb is noise to a
   screen reader. It is dropped from the first item positionally, not with
   :not(), for the reason in field guide §3.1.

   The slash is the same separator the eyebrow uses between its index and its
   label, which is what ties this to the rest of the system rather than making
   it a borrowed widget. */
.dpg-breadcrumbs{--dpg-crumb-ink:var(--dpg-slate-400);}
.dpg-breadcrumbs__list{
  display:flex;flex-wrap:wrap;align-items:center;gap:.1rem .6rem;
  margin:0;padding:0;list-style:none;
  font-family:var(--dpg-font-body);font-size:var(--dpg-fs-label);
  font-weight:500;letter-spacing:.08em;color:var(--dpg-crumb-ink);
}
.dpg-breadcrumbs__item{display:flex;align-items:center;gap:.6rem;}
.dpg-breadcrumbs__link{transition:color var(--dpg-t-fast) var(--dpg-ease);}
.dpg-breadcrumbs__item::before{content:"/";color:var(--dpg-crumb-ink);opacity:.5;}
.dpg-breadcrumbs__item:first-child::before{content:none;}
.dpg-breadcrumbs__link{color:var(--dpg-crumb-ink);text-decoration:none;}
.dpg-breadcrumbs__link:hover{color:var(--dpg-cyan);}
.dpg-breadcrumbs__current{color:var(--dpg-heading-on-dark);}
/* Inside the hero the trail sits above the badge and needs the gap the badge
   would otherwise have taken from .dpg-hero__title's top margin. */
.dpg-hero .dpg-breadcrumbs{margin-bottom:1.5rem;}

/* Triggers: short, concrete situations a reader should recognise themselves
   in. Written as prose in the source copy, lifted out here because recognition
   is the section's whole job and a reader scans before they read.

   Deliberately NOT a card, a divided grid or a ruled row — the page already
   uses a ledger (§S2) and will use a divided grid (§S8), and a third boxed
   treatment would blur all three. The only mark is the eyebrow's accent bar,
   reused at a larger width, which ties these to the system rather than making
   them a borrowed widget. Set at the H3 step in the heading face: these are
   statements to be read at a glance, not body copy. */
.dpg-triggers{display:grid;gap:clamp(1.75rem,3.5vw,2.5rem);}
.dpg-triggers__item{display:flex;flex-direction:column;gap:.9rem;}
.dpg-triggers__item::before{content:"";width:40px;height:2px;flex:none;background:var(--dpg-teal);}
.dpg-section--dark .dpg-triggers__item::before{background:var(--dpg-cyan);}
.dpg-triggers__text{
  font-family:var(--dpg-font-heading);font-size:var(--dpg-fs-h3);font-weight:500;
  line-height:var(--dpg-lh-heading);letter-spacing:-0.02em;color:var(--dpg-heading);
}
.dpg-section--dark .dpg-triggers__text{color:var(--dpg-heading-on-dark);}

/* 5.9a — Scenario ledger -------------------------------------------------- */
/* The same job as .dpg-triggers above — short, concrete situations a reader
   should recognise themselves in — but built for a NARROW column beside a
   prose argument, where .dpg-triggers is built for a wide one.

   WHY A FOURTH TREATMENT EXISTS, since the standing rule is to add a modifier
   rather than a component (build spec §9.6). Three existing ones were tried on
   dpg/spatial-why-partner §S3 and each failed for a recorded reason:
     .dpg-triggers      — an accent bar over an H3-step statement, unboxed. In
                          a narrow column three of these read as unfinished:
                          nothing separates them and nothing bounds the group.
     .dpg-steps--grid   — rules and nothing else. Right for six dense cells
                          (dpg/why-delta), too thin for three short scenarios.
     .dpg-card--lift    — a real card. Correct weight, but it made the section
                          a third boxed treatment on a page that already has
                          cards at §S4.
   What was actually needed is a ruled ledger: an index gutter so every title
   starts on the same vertical line, a hairline between rows, and a rule
   closing the group. That is the language §S2's .dpg-proof-strip--ledger
   already speaks on the sibling pillar page, so it reads as part of the system
   rather than as a fourth invention.

   Each scenario is one sentence split at its own subject/predicate boundary:
   __title takes the subject ("A new contract"), __text the rest of the same
   sentence. Nothing is rewritten — see the pattern docblock.

   Colours come from two inherited variables re-declared once for the dark
   context, so there is no specificity contest to lose (field guide §3.2).
   The light-context accent is --dpg-teal-ink, NOT --dpg-teal: the index is
   text, and --dpg-teal is 3.45:1 on white and fails AA (field guide §6.1).
   On dark, cyan is 8.7:1 on --ink, the title 17.8:1 and the text 6.9:1. */
.dpg-scenarios{
  --dpg-scenario-rule:var(--dpg-border);
  --dpg-scenario-accent:var(--dpg-teal-ink);
  display:flex;flex-direction:column;
  border-bottom:1px solid var(--dpg-scenario-rule);
}
.dpg-section--dark .dpg-scenarios{
  --dpg-scenario-rule:var(--dpg-border-on-dark);
  --dpg-scenario-accent:var(--dpg-cyan);
}
/* Fixed gutter, so all three titles align however wide the numeral renders. */
.dpg-scenarios__item{
  display:grid;grid-template-columns:2.5rem 1fr;align-items:start;
  padding-block:1.6rem;
  border-top:1px solid var(--dpg-scenario-rule);
}
.dpg-scenarios__index{
  font-family:var(--dpg-font-body);font-size:var(--dpg-fs-label);font-weight:500;
  letter-spacing:.08em;line-height:1.6;color:var(--dpg-scenario-accent);
}
.dpg-scenarios__title{
  display:block;margin-bottom:.4rem;
  font-family:var(--dpg-font-heading);font-size:var(--dpg-fs-body);font-weight:700;
  line-height:var(--dpg-lh-heading);letter-spacing:-0.01em;color:var(--dpg-heading);
}
/* A TITLE-ONLY ledger, added `Step 10av` for dpg/contact-scope. Where the copy
   gives one phrase per row and no explanation behind it, __text is simply
   absent — and the title's own bottom margin would then sit INSIDE the row as
   trailing space, making every row visibly bottom-heavy against its 1.6rem
   padding. :last-child only matches when nothing follows, so a row that DOES
   carry __text is untouched. Written as a reset on a positional selector rather
   than :not(:has(+ .dpg-scenarios__text)), which would be (0,2,0)+ and outrank
   plain rules written later — field guide §3.1. */
.dpg-scenarios__title:last-child{margin-bottom:0;}
.dpg-scenarios__text{
  display:block;
  font-size:var(--dpg-fs-small);line-height:var(--dpg-lh-lead);color:var(--dpg-text);
}
/* A heading that names the ledger (Step 16o, the services hub's "Core
   capabilities" — the copy's own H3). The tag list's label treatment: mono
   caps, slate-500 (4.76:1 on white, 4.55:1 on --tint). LIGHT BANDS ONLY — no
   dark caller exists. */
.dpg-scenarios__label{
  margin:2.25rem 0 1rem;
  font-family:var(--dpg-font-body);font-size:var(--dpg-fs-label);font-weight:500;
  letter-spacing:.12em;text-transform:uppercase;color:var(--dpg-slate-500);
}
.dpg-section--dark .dpg-scenarios__title{color:var(--dpg-scenario-accent);}
.dpg-section--dark .dpg-scenarios__text{color:var(--dpg-text-on-dark-muted);}

/* 5.9c — Facts list (labelled attributes) --------------------------------- */
/* A ruled <dl> of short label/value pairs — the "who, where, what model" block
   beside the About page's leadership prose. Added at Step 10au.

   WHY A FIFTH SMALL COMPONENT AND NOT A MODIFIER, tested against the rule in
   field guide §1.3 (a modifier is right when a component does your job in a
   different skin; a new component is right when the existing ones are built for
   a different width, weight or density):
     - .dpg-service__spec is EXACTLY this shape — a labelled <dl> — but it lives
       inside .dpg-service and is sized for a deep spec column with a 10rem
       label gutter and body-size values. Lifting it out would mean either
       restyling it (which restyles all seven cluster pages' §S4) or forking it.
     - .dpg-scenarios is a narrow ruled list, but its rows are an index + a
       title + a sentence. A fact has no index and no sentence.
     - .dpg-steps carries sequence, which a fact list must not imply.
   So: nine rules, generic, and it takes inherited variables rather than
   override rules so it works on any band (field guide §3.2).

   SEMANTICS: a real <dl>, with each pair wrapped in a <div> so the row can be
   a grid — valid HTML5 and the only way to rule a pair rather than a cell.
   Labels are mono/uppercase like every other label on the site; values are
   --dpg-fs-small so a long value does not outweigh the prose beside it. */
.dpg-facts{
  --dpg-facts-rule:var(--dpg-border);
  --dpg-facts-label:var(--dpg-slate-500);
  --dpg-facts-value:var(--dpg-heading);
  --dpg-facts-link:var(--dpg-teal-ink);
  --dpg-facts-gutter:7.5rem;
  display:block;margin:0;
}
.dpg-section--dark .dpg-facts{
  --dpg-facts-rule:var(--dpg-border-on-dark);
  --dpg-facts-label:var(--dpg-cyan);
  --dpg-facts-value:var(--dpg-text-on-dark);
  --dpg-facts-link:var(--dpg-cyan);
}
.dpg-facts__row{
  display:grid;grid-template-columns:var(--dpg-facts-gutter) 1fr;gap:1rem;align-items:baseline;
  padding:.9rem 0;border-top:1px solid var(--dpg-facts-rule);
}
.dpg-facts__row:last-child{border-bottom:1px solid var(--dpg-facts-rule);}
.dpg-facts__label{
  font-family:var(--dpg-font-body);font-size:var(--dpg-fs-label);font-weight:500;
  letter-spacing:.1em;text-transform:uppercase;color:var(--dpg-facts-label);
}
.dpg-facts__value{
  margin:0;font-size:var(--dpg-fs-small);line-height:var(--dpg-lh-lead);
  color:var(--dpg-facts-value);
}
/* LINKS INSIDE A VALUE, added `Step 10av` for dpg/contact-details (removed from
   the Contact page at `Step 19l`; its .dpg-facts--wide gutter went with it). Kept
   as the component's link style for any future caller whose values are
   tel:/mailto: rather than plain text. The base
   reset is a{color:inherit;text-decoration:none}, so without this every phone
   number and address would look exactly like static text — unclickable, in the
   one section of the site whose entire job is to be clicked.
   THE DARK HALF IS WRITTEN EVEN THOUGH NO DARK CALLER EXISTS YET. That is the
   trap registry `Step 09e` records against .dpg-prose: a class styled for one
   context only, which rendered invisibly the first time it landed in the other.
   .dpg-facts already declares a dark flip, so its links must too — --dpg-cyan
   is 8.5:1 on navy where teal-ink would go muddy.
   HOVER RESOLVES TO --dpg-facts-value, deliberately NOT --dpg-teal.
   .dpg-section__note a:hover uses --dpg-teal and is one of the two live AA
   failures still open (3.45:1 — briefing notes §2, handoff §5A/10). Copying it
   would have made three. Darkening to the value colour passes at any size on
   light and brightens to white on dark, and needs no decision from anyone. */
.dpg-facts__value a{
  color:var(--dpg-facts-link);
  text-decoration:underline;text-underline-offset:.2em;
  transition:color var(--dpg-t-fast) var(--dpg-ease);
}
.dpg-facts__value a:hover{color:var(--dpg-facts-value);}

/* 5.9b — Service spec blocks: DELETED at Step 16bc. .dpg-service rendered GIS
   cluster 1's services (image + labelled <dl> on a 10rem gutter) until that page
   moved to .dpg-card--service like the other two clusters; it had no other user.
   The history is in registry §9 (Step 10g onward). */

/* 5.10 — Audience list (who this is for) --------------------------------- */
.dpg-audience{display:grid;grid-template-columns:repeat(2,1fr);gap:2rem 3rem;margin-top:2.5rem;}
.dpg-audience__item{padding-top:1.4rem;border-top:1px solid var(--dpg-border);}
.dpg-audience__title{
  font-family:var(--dpg-font-heading);font-size:var(--dpg-fs-h3);font-weight:700;
  color:var(--dpg-heading);
}
.dpg-audience__body{margin-top:.6rem;font-size:var(--dpg-fs-small);line-height:var(--dpg-lh-lead);color:var(--dpg-text);}

/* 5.11 — Card: THE card, plus the grids that lay cards out ----------------
   Build spec §9.6 names "per-page reinvented cards" as a reference-site failure
   and requires one .dpg-card with modifiers. .dpg-cluster__card was already a
   byte-for-byte copy of what the pillar cards need, so the shared parts moved
   here and the cluster kept only its grid. Grids own COLUMNS AND GAP; the card
   owns everything inside it. Anything laying out cards reuses .dpg-card. */
.dpg-card{
  padding:2rem;border:1px solid var(--dpg-border);border-radius:var(--dpg-r-lg);
  background:var(--dpg-white);display:flex;flex-direction:column;gap:1rem;
}
.dpg-card--dark{background:var(--dpg-navy);border-color:transparent;}
.dpg-card__icon{width:1.75rem;height:1.75rem;flex:none;color:var(--dpg-teal);}
.dpg-card__title{
  font-family:var(--dpg-font-heading);font-size:var(--dpg-fs-h3);font-weight:700;
  line-height:var(--dpg-lh-heading);color:var(--dpg-heading);
}
/* A LINKED card title. Optional — a title is a heading first, so it inherits the
   heading colour at rest rather than announcing itself as a link; the card's own
   __link is the explicit affordance and this is the shortcut for people who aim
   at the title. The base reset gives every anchor an inherited colour and no
   underline (and is named in prose here, not quoted as a rule, because a
   literal brace pair inside a comment inflates the standing balance check —
   field guide §2.7), so
   without this rule a linked title would be indistinguishable from a static one
   at every state — the .dpg-facts trap of Step 10av.

   HOVER IS NOT __link's HOVER, and that is deliberate. __link goes to
   --dpg-navy on light, which is within a few units of --dpg-heading and would
   read as no change at all on a title. --dpg-teal-ink instead: 5.5:1 on white,
   4.98:1 on --tint, so it passes AA as text at any size (field guide §6.1) and
   --dpg-teal at 3.45:1 is never used here. Underline as well as colour, so the
   state is not carried by hue alone. :focus-visible already has the global
   outline at §2, so keyboard users need nothing extra. */
.dpg-card__title a{
  transition:color var(--dpg-t-fast) var(--dpg-ease);
  color:inherit;text-decoration:none;
}
.dpg-card__title a:hover{
  color:var(--dpg-teal-ink);text-decoration:underline;text-underline-offset:.15em;
}
.dpg-card__body{font-size:var(--dpg-fs-small);line-height:var(--dpg-lh-lead);color:var(--dpg-text);flex:1 1 auto;}
/* __meta: a labelled qualifier at the foot of a card, ruled off from the body.
   Added at Step 10o for the cluster pages' engagement models, where all three
   models carry a "Best for ..." line.

   WHY IT ALIGNS ACROSS CARDS WITHOUT margin-top:auto. The card is a column flex
   and __body already carries flex:1 1 auto, so the body absorbs the spare
   height and pushes __meta to the foot on its own; grid items stretch to equal
   height, so the three qualifiers land on the same line. That alignment is the
   whole point — it is what lets a reader compare three options at a glance,
   which a choose-one section needs and which the qualifier buried in prose
   cannot do. Step 09n wanted exactly this on the pillar and could not have it,
   because that copy only supplies the line for two of its three models.

   The label is the site's existing mono micro-label treatment, so it reads as a
   sibling of .dpg-eyebrow and .dpg-service__label rather than as a new object.
   --dpg-teal-ink on light, NOT --dpg-teal: a label is text and teal is 3.45:1
   (field guide §6.1). On a dark card both flip, cyan at 8.5:1. */
.dpg-card__meta{
  padding-top:.9rem;border-top:1px solid var(--dpg-border);
  font-size:var(--dpg-fs-small);line-height:var(--dpg-lh-lead);color:var(--dpg-text);
}
.dpg-card__meta-label{
  display:block;margin-bottom:.3rem;
  font-family:var(--dpg-font-body);font-size:var(--dpg-fs-label);font-weight:500;
  letter-spacing:.1em;text-transform:uppercase;color:var(--dpg-teal-ink);
}
.dpg-card--dark .dpg-card__meta{
  border-top-color:var(--dpg-border-on-dark);color:var(--dpg-text-on-dark-muted);
}
.dpg-card--dark .dpg-card__meta-label{color:var(--dpg-cyan);}
.dpg-card__link{
  transition:color var(--dpg-t-fast) var(--dpg-ease);
  display:inline-flex;align-items:center;gap:.5rem;
  font-family:var(--dpg-font-body);font-size:var(--dpg-fs-label);font-weight:500;
  letter-spacing:.06em;text-transform:uppercase;color:var(--dpg-slate-900);
}
.dpg-card__link svg{width:.85em;height:.85em;flex:none;}
/* ⚠ AA FIX (Step 15r, client). On a light card this was --dpg-teal: 3.45:1 on
   white, a fail at 12px, and the one colour in handoff §5A/10 that had stayed
   open longest. It now rests at --dpg-slate-900 (17.9:1) and hovers to
   --dpg-slate-700 (10.4:1), the client's pair. The hover is a lightening, a
   smaller step than teal-to-navy was; if it ever reads as too quiet, an
   underline on :hover is the lever, not a lighter colour. Light cards only: --dark and --lift re-declare their own colours at (0,2,0)
   further down and are untouched. */
.dpg-card__link:hover{color:var(--dpg-slate-700);}
/* On a dark card the same elements flip to the on-dark half of the palette,
   exactly as .dpg-eyebrow and .dpg-prose already do inside .dpg-section--dark. */
.dpg-card--dark .dpg-card__icon,
.dpg-card--dark .dpg-card__link{color:var(--dpg-cyan);}
.dpg-card--dark .dpg-card__title{color:var(--dpg-text-on-dark);}
/* A linked title on a dark card rests at white (inherited from the rule above)
   and hovers to --dpg-cyan at 8.5:1 on navy. NOT --dpg-white, which is what
   __link does here and would be no change at all against a white title, and not
   --dpg-teal-ink, which goes muddy on a dark ground (field guide §6.1). */
.dpg-card--dark .dpg-card__title a:hover{color:var(--dpg-cyan);}
.dpg-card--dark .dpg-card__body{color:var(--dpg-text-on-dark-muted);}
.dpg-card--dark .dpg-card__link:hover{color:var(--dpg-white);}

/* --lift: a card that sits ON a dark band rather than on a light one. --dark
   alone paints the card --dpg-navy, which is invisible against a navy section,
   so --lift replaces only the surface: the same faint white wash and hairline
   the hero badge uses, plus a 2px accent edge. Applied WITH --dark, never
   instead of it, so --dark's title, body and link colours all still apply —
   the same construction as .dpg-section--dark + --ink.

   MUST stay after the --dark block: .dpg-card--lift .dpg-card__link and
   .dpg-card--dark .dpg-card__link are both (0,2,0), so source order decides.
   Position verified in the file, not assumed (field guide §3.5).

   The accent is a variable so the three models can be told apart at a glance,
   which is what a choose-one section needs. Each accent class only re-declares
   --dpg-card-accent; border-top reads it at computed-value time, so a later
   class on the same element wins with no extra border rule. */
.dpg-card--lift{
  --dpg-card-accent:var(--dpg-cyan);
  background:rgba(255,255,255,.03);
  border-color:var(--dpg-border-on-dark);
  border-top:2px solid var(--dpg-card-accent);
}
.dpg-card--accent-lime{--dpg-card-accent:var(--dpg-lime);}
.dpg-card--accent-teal{--dpg-card-accent:var(--dpg-teal);}
/* Currently UNUSED: dpg/engagement-models dropped its per-card CTAs at Step
   09o, because all three pointed at /contact/ and the routing note beneath them
   already carries the action. Kept because --lift is a general on-dark surface
   modifier and a lifted card with a link is its obvious next use — and because
   it is exactly what restoring those CTAs would need. Lime is the site's action
   colour, 13.7:1 on navy. */
.dpg-card--lift .dpg-card__link{color:var(--dpg-lime);}
.dpg-card--lift .dpg-card__link:hover{color:var(--dpg-lime-hover);}

/* --lime (Step 15v, client): a pale lime ground (--dpg-lime-tint, #d2f69f), a
   1px --dpg-slate-800 border, and every mark in the slate 700–900 range the
   client set. Used on the Civil sub-service pages by BOTH "What we deliver"
   (*-services.php) and "Why Delta" (*-why-delta.php), which the client wants
   to share one skin. Named for the skin, not a section, for that reason — it
   was --deliverable for one step. Same construction as --dark: the modifier
   re-skins the card's own elements, so the markup is the ordinary card.
   CONTRAST, all on #d2f69f: title --dpg-slate-900 14.8:1; label and icon
   --dpg-slate-800 12.1:1; body --dpg-slate-700 8.6:1.
   ⚠ TWO DEFAULTS WOULD HAVE FAILED HERE, which is why they are re-declared and
   not left to inherit: the plain badge's --dpg-slate-500 is 3.95:1 (under 4.5
   for 12px text), and the icon's --dpg-teal is 2.87:1 (under the 3:1 floor for
   a graphic). And the BORDER IS LOAD-BEARING: the card is only 1.15:1 against
   the --tint band it sits on, so the slate-800 edge (14:1) is what draws it. */
.dpg-card--lime{background:var(--dpg-lime-tint);border-color:var(--dpg-slate-800);}
/* --lime-wash (Step 16d, client): the GIS cluster pages' "Our services" cards —
   a paler lime (--dpg-lime-wash, #e3ffb3) and NO BORDER. It shares --lime's
   slate text colours, which is why the rules below list both. On #e3ffb3:
   slate-900 16.3:1, slate-800 13.4:1, slate-700 9.5:1; slate-500 would be
   4.36:1 (a fail), so the badge goes slate-800 here too.
   ⚠ Without a border the card is only 1.04:1 against the --tint band — it reads
   by HUE, as a pale green panel, not by an edge. Not a WCAG requirement (the
   card's surface is not a control), but it is a much softer object than the
   bordered Civil card, by the client's choice. */
.dpg-card--lime-wash{background:var(--dpg-lime-wash);border-color:transparent;}
.dpg-card--lime .dpg-card__icon,
.dpg-card--lime .dpg-badge--plain,
.dpg-card--lime-wash .dpg-card__icon,
.dpg-card--lime-wash .dpg-badge--plain{color:var(--dpg-slate-800);}
.dpg-card--lime .dpg-card__title,
.dpg-card--lime-wash .dpg-card__title{color:var(--dpg-slate-900);}
.dpg-card--lime .dpg-card__body,
.dpg-card--lime-wash .dpg-card__body{color:var(--dpg-slate-700);}

/* __spec: a labelled <dl> INSIDE a card (Step 16d) — "What we deliver / Who
   this is for / How we work on this" on the GIS service cards. The card's own
   elements rather than .dpg-service__spec's, which belong to another component
   and carry a 10rem label gutter built for a full-width row. Each pair sits in
   a <div> (valid in a <dl>) so it can carry its rule and be one grid row.
   Label on --dpg-teal-ink by default (5.5:1 on white), text at the card body's
   size; the lime skins re-colour both below. */
.dpg-card__spec{margin:0;}
.dpg-card__spec-row{margin-top:1.25rem;padding-top:1rem;border-top:1px solid var(--dpg-border);}
.dpg-card__spec-label{
  font-family:var(--dpg-font-body);font-size:var(--dpg-fs-label);font-weight:500;
  letter-spacing:.1em;text-transform:uppercase;color:var(--dpg-teal-ink);
}
.dpg-card__spec-text{margin:.4rem 0 0;font-size:var(--dpg-fs-small);line-height:var(--dpg-lh-lead);color:var(--dpg-text);}
.dpg-card--lime .dpg-card__spec-row,
.dpg-card--lime-wash .dpg-card__spec-row{border-top-color:var(--dpg-slate-800);}
.dpg-card--lime .dpg-card__spec-label,
.dpg-card--lime-wash .dpg-card__spec-label{color:var(--dpg-slate-800);}
.dpg-card--lime .dpg-card__spec-text,
.dpg-card--lime-wash .dpg-card__spec-text{color:var(--dpg-slate-700);}

/* --service (Step 16d): cards in a .dpg-reasons row whose parts LINE UP across
   the row — every card's summary, and every one of its spec rows, starts at the
   same height as its neighbour's, however long the text above it. Done with
   subgrid: the card spans the grid rows its parts need and adopts them, and its
   <dl> does the same one level down. The span is FIXED for the markup it was
   built for — icon, badge, title, summary, then a three-pair <dl> = 4 + 3 = 7
   rows. A card with a different pair count needs its own span.
   Without subgrid support the card falls back to the ordinary flex column: the
   same content, simply not aligned across the row. */
@supports (grid-template-rows:subgrid){
  .dpg-reasons > .dpg-card--service{
    display:grid;grid-template-rows:subgrid;grid-row:span 7;row-gap:1rem;
  }
  .dpg-card--service > .dpg-card__spec{
    display:grid;grid-template-rows:subgrid;grid-row:span 3;row-gap:0;
  }
  /* --spec-loose (Step 16bc): a pair whose two cards carry DIFFERENT spec
     labels or counts (GIS cluster 1: three pairs against four). Aligning every
     row would set unlike labels side by side and open gaps, so the card spans
     only icon, badge, title, summary and the <dl> (5 rows) — those still line
     up across the pair — and the <dl> stacks its own rows as a plain block.
     The pattern decides which pairs are loose by comparing the labels. Same
     specificity as the two rules above and later, so it wins by order. */
  .dpg-reasons > .dpg-card--spec-loose{grid-row:span 5;}
  .dpg-card--spec-loose > .dpg-card__spec{display:block;grid-row:auto;}
}
/* A card FOOTNOTE on the lime skins (Step 16bc): .dpg-card__meta, ruled off at
   the card's foot, carrying a qualifier scoped to that one card (GIS cluster
   1's service 3). Slate like the rest of the skin — slate-700 is 9.5:1 on
   --lime-wash — with the spec rows' slate-800 rule. */
.dpg-card--lime .dpg-card__meta,
.dpg-card--lime-wash .dpg-card__meta{margin-top:1.75rem;border-top-color:var(--dpg-slate-800);color:var(--dpg-slate-700);}

/* THE ODD ONE OUT GOES WIDE (Step 16e). Service cards pair up two to a row;
   when the count is odd, the last card — which is also the ONLY card on a
   one-service page — spans the whole row instead of sitting half-empty, and
   its spec rows turn from a stack into COLUMNS (as many as fit at 15rem, so one
   on a phone), because a full-width card with stacked rows is a long thin
   strip. It leaves the subgrid pairing (the rules above) for its own layout,
   which is why display, grid-row and grid-template-rows are reset here.
   Positional on purpose, so a page adapts to its service count with no class
   to remember; (0,4,0) and (0,5,0) outrank the pairing rules by design — this
   is a stronger case, not a specificity fight (field guide §3.1 is about
   :not() resets, which this is not). */
.dpg-reasons > .dpg-card--service:last-child:nth-child(odd){
  grid-column:1 / -1;display:flex;grid-row:auto;grid-template-rows:none;
}
.dpg-reasons > .dpg-card--service:last-child:nth-child(odd) > .dpg-card__spec{
  display:grid;grid-template-columns:repeat(auto-fit,minmax(15rem,1fr));
  column-gap:2rem;grid-row:auto;grid-template-rows:none;
}

/* A PROOF ROW (Step 16e) — a spec pair that is Delta's demonstrated work, not a
   capability claim (build spec §1: the evidence tiers are never blended). Its
   label becomes a navy chip with a check, its text steps up to slate-900 at
   500, and its rule doubles. Set after the lime skins' spec colours so it wins
   at equal specificity. Lime on navy 13.7:1; slate-900 on --lime-wash 16.3:1. */
.dpg-card__spec-row--proof{border-top-width:2px;}
.dpg-card__spec-row--proof .dpg-card__spec-label{
  display:inline-flex;align-items:center;gap:.4rem;
  padding:.3rem .6rem;border-radius:var(--dpg-r-xs);
  background:var(--dpg-navy);color:var(--dpg-lime);
}
.dpg-card__spec-row--proof .dpg-card__spec-label svg{width:.8rem;height:.8rem;flex:none;}
.dpg-card__spec-row--proof .dpg-card__spec-text{font-weight:500;color:var(--dpg-slate-900);}
/* The muted half of a two-tone claim (the GIS Why-Delta cards) is slate-500 by
   default, 3.95:1 here. No Civil card uses it today; this keeps the skin from
   failing the day one does. */
.dpg-card--lime .dpg-card__counter,
.dpg-card--lime-wash .dpg-card__counter{color:var(--dpg-slate-700);}
/* A chip is a flex ITEM here; .dpg-badge already carries align-self:flex-start
   so it hugs its text instead of stretching the column. */
.dpg-card > .dpg-badge--lime{margin-bottom:1.25rem;}

/* --row: THE card with its box removed and turned on its side. Used where a
   card holds more text than a narrow column can carry — the GIS pillar's three
   cluster teasers each have a compound title, ~50 words of body and a long
   descriptive link, which in a 3-up grid wraps the title to three lines and the
   link to three more. The content wants width, so the layout gives it width.
   It also stops the pillar repeating the homepage's 3-up card grid one click
   away from it.

   Everything INSIDE is inherited — __title, __body and __link are untouched
   except for the body's size, which steps up from --small to --body now that it
   has room. That is what keeps this a modifier and not a second card.

   Placement is done with explicit grid coordinates rather than a wrapper div:
   the title spans both rows of column 1, the body and link stack in column 2.
   No extra markup, so the same children work in both forms of the card. */
.dpg-card--row{
  display:grid;grid-template-columns:2fr 3fr;
  gap:1.25rem clamp(1.5rem,4vw,3.5rem);
  padding:clamp(1.75rem,3.5vw,2.5rem) 0;
  border:0;border-top:1px solid var(--dpg-border);border-radius:0;background:none;
}
/* Three rows: the heading spans the full width, then the media column and the
   text column sit beneath it. The media spans both remaining rows so the image
   is as tall as the body-and-link stack beside it. Coordinates are explicit
   rather than relying on a wrapper div, so .dpg-card's children keep working in
   both forms of the card — that is what makes --row a modifier. */
.dpg-card--row .dpg-card__title{grid-column:1 / -1;grid-row:1;}
.dpg-card--row .dpg-card__media{grid-column:1;grid-row:2 / span 2;}
.dpg-card--row .dpg-card__body{
  grid-column:2;grid-row:2;flex:none;
  font-size:var(--dpg-fs-body);line-height:var(--dpg-lh-body);
}
.dpg-card--row .dpg-card__link{grid-column:2;grid-row:3;align-self:end;}

/* Card media. The <picture> is a normal block here — .dpg-hero picture's
   display:contents is scoped to the hero and does not reach this. The image
   fills its column and keeps its own aspect ratio; width/height on the <img>
   reserve the box, so adding these three images costs no CLS. */
.dpg-card__media{margin:0;}
.dpg-card__img{
  display:block;width:100%;height:auto;
  border-radius:var(--dpg-r-md);
}

/* --media-top (Step 16g): THE card with its image run edge to edge across the
   top — the Mechanical pillar's six practice areas. The card drops its own
   padding and gap and hands them to a __content wrapper, because the image must
   touch the card's border on three sides while the text keeps the 2rem-family
   inset; overflow:hidden lets the card's own radius clip the image's top
   corners, so the image itself carries no radius. The picture is cropped to one
   16:10 ratio whatever its source (2:1, 16:9, 3:2 today), so every card in a
   row starts its text on the same line. __content is flex:1 so equal-height
   grid cards keep their text top-aligned. Everything else — __title, __body,
   the plain badge — is inherited unchanged.
   __img--right shifts the crop to the image's right edge; it exists for one
   PLACEHOLDER (the homepage collage, whose left third is empty navy) and goes
   when that slot gets real artwork. */
.dpg-card--media-top{padding:0;gap:0;overflow:hidden;}
.dpg-card--media-top .dpg-card__media{border-bottom:1px solid var(--dpg-border);}
.dpg-card--media-top .dpg-card__img{aspect-ratio:16 / 10;object-fit:cover;border-radius:0;}
.dpg-card__img--right{object-position:right center;}
.dpg-card--media-top .dpg-card__content{
  flex:1 1 auto;display:flex;flex-direction:column;gap:.75rem;
  padding:1.5rem 1.75rem 2rem;
}
/* A linked card (Civil, Step 16h): the link hugs its text instead of stretching
   the flex column, so the click target is the words, and sits a step below the
   body, which (flex:1) has already pushed it to the card's foot. It is BLOCK
   with an inline arrow, not the base inline-flex: these "Explore …" labels wrap
   to two lines in a third-width card, and in a flex row the wrapped text becomes
   one full-width item that strands the arrow at the far edge (seen in the
   Step 16h render). Inline, the arrow follows the last word. */
.dpg-card--media-top .dpg-card__link{display:block;align-self:flex-start;margin-top:.5rem;}
.dpg-card--media-top .dpg-card__link svg{display:inline-block;margin-left:.45rem;vertical-align:-.05em;}

/* Cluster teasers (pillar → cluster-page links). A STACK, not a grid: the cards
   are --row, so the list owns only the closing rule under the last one. */
.dpg-cluster{display:grid;margin-top:2.75rem;}
.dpg-cluster > .dpg-card--row:last-child{border-bottom:1px solid var(--dpg-border);}

/* Pillar cards (the three capability pillars; home + services hub). Tighter gap
   and a wider gap above than the cluster grid — both read off the mockup, and
   both are component layout, which build spec §7.1 puts outside the token set. */
.dpg-pillar-cards{display:grid;grid-template-columns:repeat(3,1fr);gap:1rem;margin-top:4rem;}

/* 5.11a — Case study band ------------------------------------------------
   The section clips its own watermark, so it owns the stacking context. */
.dpg-case{position:relative;overflow:hidden;}
.dpg-case > .dpg-container{position:relative;z-index:1;}

/* Oversized client name behind the heading. ABSOLUTELY POSITIONED ON PURPOSE:
   it is out of flow, so editing the word, lengthening it, or deleting the
   element entirely moves nothing else on the page. aria-hidden because it only
   repeats a name the eyebrow and heading already carry, and unselectable so it
   never lands in a copied block of text. top/right are the two knobs, both in
   em so the offset tracks the clamped font-size instead of drifting at one
   end; .15em drops the cap tops ~49px below the section edge at full size. */
.dpg-case__watermark{
  position:absolute;top:.15em;right:-.04em;z-index:0;
  font-family:var(--dpg-font-heading);
  font-size:clamp(4rem,16vw,14rem);
  font-weight:700;line-height:.8;letter-spacing:-0.04em;
  text-transform:uppercase;white-space:nowrap;
  color:var(--dpg-white);opacity:.6;
  pointer-events:none;-webkit-user-select:none;user-select:none;
}

/* Heading left, "read the full case study" pinned to the right and sitting on
   the heading's last line. Wraps to its own row on narrow screens. */
.dpg-case__head{display:flex;flex-wrap:wrap;align-items:flex-end;justify-content:space-between;gap:1.5rem 2rem;}
.dpg-case__head > .dpg-section__head{flex:1 1 34rem;}
.dpg-case__cta{
  transition:color var(--dpg-t-fast) var(--dpg-ease);
  display:inline-flex;align-items:center;gap:.5rem;
  font-weight:600;color:var(--dpg-heading);
  text-decoration:underline;text-decoration-thickness:1px;text-underline-offset:.5em;
}
.dpg-case__cta svg{width:1em;height:1em;flex:none;}
.dpg-case__cta:hover{color:var(--dpg-teal-ink);}

/* Four columns: the featured metric takes the left half as a 2x2 block, the
   four supporting metrics fill the right half. Cards are .dpg-card. */
.dpg-case__metrics{display:grid;grid-template-columns:repeat(4,1fr);gap:1rem;margin-top:3rem;}
.dpg-case__metric--featured{grid-column:span 2;grid-row:span 2;}
.dpg-case__figure{
  font-family:var(--dpg-font-heading);font-size:var(--dpg-fs-h3);font-weight:700;
  line-height:var(--dpg-lh-tight);letter-spacing:-0.03em;color:var(--dpg-heading);
}
.dpg-case__metric--featured .dpg-case__figure{font-size:var(--dpg-fs-stat-xl);}
/* margin-top:auto drops the label to the card's bottom edge however tall the
   card grows — which is what keeps the featured card's label anchored. */
.dpg-case__label{
  margin-top:auto;font-family:var(--dpg-font-body);font-size:var(--dpg-fs-label);
  font-weight:500;letter-spacing:.04em;color:var(--dpg-slate-500);
}
.dpg-card--dark .dpg-case__figure{color:var(--dpg-text-on-dark);}
.dpg-card--dark .dpg-case__label{color:var(--dpg-slate-400);}
.dpg-case__body{margin-top:4rem;}

/* 5.11b — Comparison table (Without / With Delta) ------------------------
   A real <table> with a <caption> and scoped headers, per build spec §10. The
   row label is the axis, so it is <th scope="row">; the two claim columns are
   <th scope="col">. A screen reader then announces each cell in full — "Staffing
   focus, Without Managed Production Capacity, Senior staff absorbed into…" —
   which a grid of <div>s cannot do at any amount of ARIA.
   The wrapper scrolls rather than reflowing: two columns of prose cannot usefully
   compress below ~46rem, and a scrolled table beats a broken one. */
.dpg-comparison{
  margin-top:3rem;overflow-x:auto;
  border:1px solid var(--dpg-border);border-radius:var(--dpg-r-lg);
}
.dpg-comparison__table{width:100%;min-width:46rem;border-collapse:collapse;text-align:left;}
.dpg-comparison__table th,
.dpg-comparison__table td{padding:1.4rem 1.5rem;vertical-align:top;border-top:1px solid var(--dpg-border);}
.dpg-comparison__table thead th{border-top:0;}

/* Column 1 is the axis, and takes the same mono label treatment as every other
   muted label on the site (eyebrow, proof-strip label, metric label). */
.dpg-comparison__axis{
  background:var(--dpg-slate-50);
  font-family:var(--dpg-font-body);font-size:var(--dpg-fs-label);font-weight:500;
  letter-spacing:.1em;text-transform:uppercase;color:var(--dpg-slate-500);
  white-space:nowrap;
}

/* The two claim columns are identical in every respect but hue. */
.dpg-comparison__head--without,
.dpg-comparison__cell--without{background:var(--dpg-without-bg);color:var(--dpg-without-text);}
.dpg-comparison__head--with,
.dpg-comparison__cell--with{background:var(--dpg-with-bg);color:var(--dpg-with-text);}

.dpg-comparison__head{
  font-family:var(--dpg-font-heading);font-size:var(--dpg-fs-body);font-weight:700;
  line-height:var(--dpg-lh-heading);
}
.dpg-comparison__claim{
  display:block;font-family:var(--dpg-font-heading);font-size:var(--dpg-fs-body);
  font-weight:700;line-height:var(--dpg-lh-heading);
}
/* The note stays in the cell's own hue, stepped back rather than switched to a
   neutral grey, so each column reads as one colour family. .85 holds both above
   5:1 against their surfaces. */
.dpg-comparison__note{
  display:block;margin-top:.5rem;
  font-size:var(--dpg-fs-small);line-height:var(--dpg-lh-lead);opacity:.85;
}

/* 5.12 — Numbered steps (how we work) ------------------------------------ */
.dpg-steps{margin-top:2.5rem;display:flex;flex-direction:column;}
/* Every rule this component draws reads --dpg-steps-rule, which is re-declared
   once on a dark band. Colouring each border individually and then trying to
   override them all from .dpg-section--dark is what made the grid's verticals
   brighter than its horizontals: .dpg-steps--grid .dpg-steps__item:not(...) is
   (0,3,0) and beat the (0,2,0) dark override. One inherited variable removes the
   specificity contest entirely. */
.dpg-steps{--dpg-steps-rule:var(--dpg-border);}
.dpg-section--dark .dpg-steps{--dpg-steps-rule:var(--dpg-border-on-dark);}
.dpg-steps__item{
  display:flex;gap:1.5rem;padding-block:1.6rem;
  border-top:1px solid var(--dpg-steps-rule);
}
.dpg-steps__index{
  flex:none;width:2.25rem;font-family:var(--dpg-font-body);font-size:var(--dpg-fs-label);
  font-weight:500;color:var(--dpg-slate-400);padding-top:.2rem;
}
.dpg-steps__body{max-width:640px;}
.dpg-steps__lead{font-weight:600;color:var(--dpg-heading);}
.dpg-steps__text{font-size:var(--dpg-fs-small);line-height:var(--dpg-lh-lead);color:var(--dpg-text);}
/* --closed bounds the group with a rule under the last item; kept separate from
   --boxed because both step variants want it and neither wants the other's
   layout. --boxed is the in-column form: no top margin, lead above its text. */
.dpg-steps--closed > .dpg-steps__item:last-child{border-bottom:1px solid var(--dpg-steps-rule);}
.dpg-steps--boxed{margin-top:0;}
/* Numbered steps run lead and text together as one paragraph; here the lead is
   a heading for the line beneath it, so it becomes a block. */
.dpg-steps--boxed .dpg-steps__lead{display:block;margin-bottom:.35rem;}
.dpg-steps--boxed .dpg-steps__index{width:1.75rem;color:var(--dpg-teal-ink);}
.dpg-steps--boxed .dpg-steps__index svg{width:1.15em;height:1.15em;display:block;}

/* --split is the full-width form: index | title | text as three columns. The
   row is already a flex pair (index + body), so only __body becomes a grid —
   no extra wrapper, and the markup is identical to every other step list. */
.dpg-steps--split .dpg-steps__item{padding-block:2rem;}
.dpg-steps--split .dpg-steps__index{width:8.5rem;color:var(--dpg-teal-ink);padding-top:.45rem;}
.dpg-steps--split .dpg-steps__body{
  display:grid;grid-template-columns:15rem 1fr;gap:.5rem 2.5rem;max-width:none;
}
.dpg-steps--split .dpg-steps__lead{
  font-family:var(--dpg-font-heading);font-size:var(--dpg-fs-h3);font-weight:700;
  line-height:var(--dpg-lh-heading);
}
.dpg-steps--split .dpg-steps__text{font-size:var(--dpg-fs-body);line-height:var(--dpg-lh-body);}

/* --grid is the same numbered item laid out as a cell rather than a row: index
   above title above text, three across. Dividers are borders on the cells
   themselves (as the proof strip does) so they span the full cell height and
   vanish correctly when the grid reflows; the closing rule sits on the container
   because :last-child is one cell, not a row. Column 1 and 3 lose their outer
   padding so the grid's content lines up with every other section's gutter. */
.dpg-steps--grid{
  display:grid;grid-template-columns:repeat(3,1fr);
  border-bottom:1px solid var(--dpg-steps-rule);
}
.dpg-steps--grid .dpg-steps__item{flex-direction:column;gap:.75rem;padding:2rem 2rem 3.5rem;}
/* border-left is set on EVERY cell and removed from column 1, rather than set
   with :not(:nth-child()). The :not() form is (0,3,0), which no plain rule in
   a media query can switch off — that is why the verticals survived the mobile
   collapse. This form is (0,2,0) and yields to the breakpoint below. */
.dpg-steps--grid .dpg-steps__item{border-left:1px solid var(--dpg-steps-rule);}
.dpg-steps--grid .dpg-steps__item:nth-child(3n+1){border-left:0;padding-inline-start:0;}
.dpg-steps--grid .dpg-steps__item:nth-child(3n){padding-inline-end:0;}
.dpg-steps--grid .dpg-steps__index{width:auto;padding-top:0;}
.dpg-steps--grid .dpg-steps__lead{
  display:block;margin-bottom:.4rem;
  font-family:var(--dpg-font-heading);font-size:var(--dpg-fs-h3);font-weight:700;
  line-height:var(--dpg-lh-heading);
}
.dpg-steps--grid .dpg-steps__text{font-size:var(--dpg-fs-small);line-height:var(--dpg-lh-lead);}

/* Step lists on a dark band, flipping to the on-dark half of the palette exactly
   as .dpg-eyebrow, .dpg-section__title and .dpg-prose already do. Placed AFTER
   the variants above so it outranks --split's teal index at equal specificity:
   on navy, cyan is 8.5:1 and the teal ink would go muddy. */
.dpg-section--dark .dpg-steps__index{color:var(--dpg-cyan);}
.dpg-section--dark .dpg-steps__lead{color:var(--dpg-text-on-dark);}
.dpg-section--dark .dpg-steps__text{color:var(--dpg-text-on-dark-muted);}

/* 5.13 — Feature grid (capability lists) --------------------------------- */
.dpg-feature-grid{margin-top:2.5rem;display:grid;grid-template-columns:repeat(2,1fr);gap:clamp(2.5rem,5vw,4.5rem);}

/* EVIDENCE TIERS ARE THE POINT OF THIS COMPONENT (build spec §1), so the two
   groups must never read as one list. Three signals carry the difference and
   all three are driven by inherited variables rather than override rules:

     label  the group heading
     mark   the icon — a CHECK for work that is done, a PLUS for work that is
            available. This is semantic, not decoration: a check against an
            undelivered capability asserts it has been delivered, which is the
            exact tier blend §1 forbids.
     text   the item colour; the proof group sits at full strength, the
            capability group one step back

   Re-declaring three custom properties on a group beats any selector contest,
   because a group is a descendant of the grid and its own declaration simply
   wins for its subtree (field guide §3.2). */
.dpg-feature-grid{
  --dpg-fg-label:var(--dpg-slate-500);
  --dpg-fg-mark:var(--dpg-slate-400);
  --dpg-fg-text:var(--dpg-text);
}
.dpg-feature-grid__group--proof{
  --dpg-fg-label:var(--dpg-teal-ink);
  --dpg-fg-mark:var(--dpg-teal);
  --dpg-fg-text:var(--dpg-heading);
}
/* --fill: a group painted as a solid panel. Every colour inside it flips to the
   band's own background, which is what makes one declaration enough — the three
   variables already drive the label, the mark and the item text, so nothing
   needs a per-property override. Navy on lime is 13.7:1.

   Padding sits on BOTH groups, not just the filled one: give it only to --fill
   and its group title starts a padding's worth lower than its neighbour's, and
   the two columns visibly stop lining up. feature-grid.php is the component's
   only user today; a future unfilled use would want this reconsidered. */
.dpg-feature-grid__group{padding:clamp(1.25rem,2.5vw,1.75rem);}
.dpg-feature-grid__group--fill{
  background:var(--dpg-lime);
  border-radius:var(--dpg-r-lg);
  --dpg-fg-label:var(--dpg-navy);
  --dpg-fg-mark:var(--dpg-navy);
  --dpg-fg-text:var(--dpg-navy);
}
.dpg-section--dark .dpg-feature-grid{
  --dpg-fg-label:var(--dpg-slate-400);
  --dpg-fg-mark:var(--dpg-slate-500);
  --dpg-fg-text:var(--dpg-text-on-dark-muted);
}
.dpg-section--dark .dpg-feature-grid__group--proof{
  --dpg-fg-label:var(--dpg-cyan);
  --dpg-fg-mark:var(--dpg-cyan);
  --dpg-fg-text:var(--dpg-heading-on-dark);
}
/* --single: ONE evidence tier instead of two. Added at Step 10s for GIS cluster
   page 2, whose §S6 has no "demonstrated" group at all — Delta has no delivered
   automation work, and the copy is honest about that rather than padding the
   column. Two groups in a 2-up grid is the component's normal case; one group
   would leave half the row empty, so the grid collapses to one column and the
   ITEM LIST goes 2-up instead. Seven short capability names read better in two
   columns than as one tall list, and the section keeps the same visual weight
   as its two-tier sibling on cluster page 1.

   Still needs a dark band, for the same measured reason as the two-tier case:
   an unfilled group's mark inherits the grid's base colour, which is 2.45:1 on
   tint and 3.66:1 on navy (Step 10k). */
.dpg-feature-grid--single{grid-template-columns:1fr;}
.dpg-feature-grid--single .dpg-feature-grid__list{
  display:grid;grid-template-columns:repeat(2,1fr);gap:.9rem 2.5rem;
}
.dpg-feature-grid__group-title{
  font-family:var(--dpg-font-body);font-size:var(--dpg-fs-label);font-weight:500;
  letter-spacing:.1em;text-transform:uppercase;color:var(--dpg-fg-label);margin-bottom:1.2rem;
}
.dpg-feature-grid__list{display:flex;flex-direction:column;gap:.9rem;}
.dpg-feature-grid__item{
  display:flex;align-items:flex-start;gap:.7rem;
  font-size:var(--dpg-fs-small);line-height:var(--dpg-lh-lead);color:var(--dpg-fg-text);
}
.dpg-feature-grid__icon{flex:none;width:1.1rem;height:1.1rem;margin-top:.2rem;color:var(--dpg-fg-mark);}

/* The negated half of an antithesis claim, inside __title. Every §S8 claim is
   built the same way — "a partner in the outcome, NOT a vendor for the task",
   "tracked, NOT assumed" — and that structure is invisible when the sentence is
   set in one colour. Two tones make the rhetoric legible at a glance.

   --dpg-slate-500, NOT slate-400: this is real sentence text, so it must pass
   AA as text. slate-500 is 4.76:1 on white and passes at any size; slate-400 is
   2.5:1 and would fail even though the claim is set large. */
.dpg-card__counter{color:var(--dpg-slate-500);}
.dpg-card--dark .dpg-card__counter{color:var(--dpg-text-on-dark-muted);}

/* Reasons grid: 2x2, because four items in a 3-up grid leave a hole and this
   page has no boxed-card section yet — .dpg-cluster's cards are --row, which
   has no box at all. Grid owns columns and gap only; the card owns its inside. */
.dpg-reasons{
  display:grid;grid-template-columns:repeat(2,1fr);
  gap:clamp(1.25rem,2.5vw,2rem);margin-top:2.75rem;
}
/* --lead: the first reason spans both columns and steps up to the H2 size.
   Added at Step 10l for the cluster pages, whose §S7 has FIVE claims where the
   pillar's has four — and five in a 2x2 leaves a hole. One-spanning-plus-2x2
   fills the grid and happens to match the copy's own hierarchy, since the first
   claim is the positioning thesis the other four support.

   :first-child is safe here where field guide §3.1 warns off positional
   selectors: that warning is about :not(:nth-child()) inheriting its argument's
   specificity and outranking later plain rules. This is (0,2,0), it sets rather
   than resets, and grid-column:1/-1 stays correct after the 900px collapse
   because spanning a single column is a no-op. Nothing to reset.

   The body is capped because the lead card is full width and its text would
   otherwise run the whole container at --dpg-fs-small. 820px matches
   .dpg-section__head. */
.dpg-reasons--lead > :first-child{grid-column:1 / -1;}
.dpg-reasons--lead > :first-child .dpg-card__title{
  font-size:var(--dpg-fs-h2);letter-spacing:-0.03em;
}
.dpg-reasons--lead > :first-child .dpg-card__body{max-width:820px;}

/* 5.14 — Card grid (3-up) -------------------------------------------------- */
/* GRID ONLY — columns, gap and top margin. The cards inside are .dpg-card with
   whatever modifiers the section needs; this owns no card styling at all.

   Renamed from .dpg-engagement at Step 10c. It was named for its first user,
   dpg/engagement-models, and acquired a second one — dpg/spatial-why-partner's
   trigger cards — at which point the name was actively misleading about what it
   lays out. Renaming beat adding a fourth near-identical columns-and-gap grid
   beside .dpg-pillar-cards (3-up, 1rem) and .dpg-reasons (2-up).

   History: __card, __title, __body and __link once lived here and were a
   byte-for-byte copy of .dpg-card apart from two values — a slate-50 fill and a
   hardcoded 1.15rem title — and build spec §9.6 allows exactly one card. Folded
   into .dpg-card at Step 09n; the 1.15rem was a leftover rather than a decision,
   so the titles now sit on --dpg-fs-h3 like every other card title. This was the
   last duplicate card in the codebase. */
.dpg-card-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:2rem;margin-top:2.75rem;}
/* --media (Step 16g): SIX image cards (the Mechanical practice areas), which the
   base grid's single 900px collapse would turn into one very long column on a
   tablet. One column below 640px, two to 1099px, three from 1100px — the post
   grid's steps. Two-class selectors so the result does not depend on source
   order against the base rule's collapse inside §7's 900px query. The picture
   `sizes` in dpg/mech-practice-areas is derived from these values. */
.dpg-card-grid.dpg-card-grid--media{grid-template-columns:minmax(0,1fr);gap:1.5rem;}
@media (min-width:640px){
  .dpg-card-grid.dpg-card-grid--media{grid-template-columns:repeat(2,minmax(0,1fr));}
}
@media (min-width:1100px){
  .dpg-card-grid.dpg-card-grid--media{grid-template-columns:repeat(3,minmax(0,1fr));gap:2rem;}
}
/* --lead (Step 16h): the FIRST card spans the whole row — seven practice areas on
   the Civil pillar, which would otherwise leave one card alone on the last row.
   With the six after it the rows are even at 3, 2 and 1 columns. From 900px a
   --media-top lead card turns on its side: image left (5fr), text right (6fr),
   the image filling its column's height (absolutely placed in a relatively
   positioned figure, floored at 20rem) and the text centred against it.
   Positional, so it needs no class on the card; the same idea as
   .dpg-reasons--lead. The picture `sizes` in dpg/civil-practice-areas is derived
   from the 5fr column. */
.dpg-card-grid--lead > .dpg-card:first-child{grid-column:1 / -1;}
@media (min-width:900px){
  .dpg-card-grid--lead > .dpg-card--media-top:first-child{
    display:grid;grid-template-columns:minmax(0,5fr) minmax(0,6fr);
  }
  .dpg-card-grid--lead > .dpg-card--media-top:first-child .dpg-card__media{
    position:relative;min-height:20rem;border-bottom:0;border-right:1px solid var(--dpg-border);
  }
  .dpg-card-grid--lead > .dpg-card--media-top:first-child .dpg-card__img{
    position:absolute;inset:0;height:100%;aspect-ratio:auto;
  }
  .dpg-card-grid--lead > .dpg-card--media-top:first-child .dpg-card__content{
    justify-content:center;padding:2.5rem clamp(2rem,4vw,3.5rem);
  }
  .dpg-card-grid--lead > .dpg-card--media-top:first-child .dpg-card__body{flex:none;}
}

/* 5.15 — FAQ (native details/summary, no JS) ------------------------------ */
.dpg-faq{margin-top:2.5rem;max-width:820px;}
.dpg-faq__item{border-top:1px solid var(--dpg-border);}
.dpg-faq__item:last-child{border-bottom:1px solid var(--dpg-border);}
.dpg-faq__q{
  padding-block:1.4rem;cursor:pointer;list-style:none;
  display:flex;align-items:center;justify-content:space-between;gap:1rem;
  font-weight:600;color:var(--dpg-heading);
}
.dpg-faq__q::-webkit-details-marker{display:none;}
.dpg-faq__icon{flex:none;width:1rem;height:1rem;transition:transform var(--dpg-t-fast) var(--dpg-ease);color:var(--dpg-slate-400);}
.dpg-faq__item[open] .dpg-faq__icon{transform:rotate(45deg);}
.dpg-faq__a{padding-bottom:1.4rem;font-size:var(--dpg-fs-small);line-height:var(--dpg-lh-lead);color:var(--dpg-text);max-width:70ch;}

/* --numbered is the full-width indexed form: an index column before the
   question, a heading-sized question, and no measure cap. The GIS pillar's
   dpg/faq keeps the compact unnumbered form — same component, two seeds.
   align-items:flex-start rather than centre so the index and the + stay on the
   question's FIRST line when it wraps to two. */
.dpg-faq--numbered{max-width:none;}
.dpg-faq--numbered .dpg-faq__q{padding-block:2rem;gap:1.5rem;align-items:flex-start;}
.dpg-faq--numbered .dpg-faq__index{
  flex:none;width:4rem;padding-top:.5rem;
  font-family:var(--dpg-font-body);font-size:var(--dpg-fs-label);font-weight:500;
  color:var(--dpg-teal-ink);
}
.dpg-faq--numbered .dpg-faq__text{
  flex:1 1 auto;
  font-family:var(--dpg-font-heading);font-size:var(--dpg-fs-h3);font-weight:700;
  line-height:var(--dpg-lh-heading);
}
.dpg-faq--numbered .dpg-faq__icon{width:1.25rem;height:1.25rem;margin-top:.35rem;color:var(--dpg-heading);}
/* The answer aligns under the question, not under the index: 4rem column + the
   1.5rem gap. Keep the two in step if either changes. */
.dpg-faq--numbered .dpg-faq__a{padding-left:5.5rem;font-size:var(--dpg-fs-body);}

/* 5.16 — Anchor nav (sticky in-page section jump bar) --------------------- */
.dpg-anchor-nav{
  position:sticky;top:72px;z-index:var(--dpg-z-anchor);
  background:var(--dpg-navbar-bg);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);
  border-bottom:1px solid var(--dpg-border);
}
.dpg-anchor-nav__inner{
  display:flex;gap:1.75rem;overflow-x:auto;
  padding-block:1rem;scrollbar-width:none;
}
.dpg-anchor-nav__inner::-webkit-scrollbar{display:none;}
.dpg-anchor-nav__link{
  flex:none;font-family:var(--dpg-font-body);font-size:var(--dpg-fs-label);font-weight:500;
  letter-spacing:.06em;text-transform:uppercase;color:var(--dpg-slate-500);
  transition:color var(--dpg-t-fast) var(--dpg-ease);
}
.dpg-anchor-nav__link:hover,
.dpg-anchor-nav__link[aria-current="true"]{color:var(--dpg-teal);}

/* 5.17 — Split narrative band (statement left, prose right) --------------
   Two equal columns: .dpg-section__head (eyebrow + statement) and .dpg-prose.
   Both children are existing components - this adds only the split itself. */
.dpg-split{display:grid;grid-template-columns:1fr 1fr;gap:clamp(2rem,4vw,4rem);align-items:start;}
/* When the second column carries a list and the first only prose, 50/50 starves
   the list. Collapses to one column with .dpg-split at 900px. */
.dpg-split--wide-end{grid-template-columns:2fr 3fr;}

/* A column that stays put while the taller column beside it moves past.

   WHY STICKY AND NOT A NESTED SCROLLER. The obvious literal reading of "only
   that column scrolls" is overflow-y:auto on the other one. That is the wrong
   tool three times over: a scrollable region must be focusable and named to be
   keyboard-operable at all (WCAG 2.1.1 — without tabindex a keyboard user
   simply cannot reach the overflow), it traps the mouse wheel, and it is poor
   on touch. Sticky produces the same sensation with everything left in normal
   document flow: nothing to focus, nothing to name, nothing to trap, and the
   page continues the moment the tall column runs out.

   It works here because .dpg-split is align-items:start, so the item is
   content-height while its containing block is the full grid row — the element
   must be SHORTER than its containing block or it has no range to travel
   (field guide §2.2, the same rule that kept the header from sticking).

   top clears the 72px sticky header with room to spare. Released below 900px,
   where .dpg-split has already collapsed to one column and a sticky panel would
   sit on top of the content it introduces. */
.dpg-split__sticky{position:sticky;top:6rem;}
.dpg-split__sticky .dpg-btn{margin-top:2rem;}
/* Inside a split the FAQ fills its column and needs no top margin of its own. */
.dpg-split .dpg-faq{max-width:none;margin-top:0;}

/* A PHOTO column beside the text (Step 16o, the services hub's pillar bands).
   The <picture> is a normal block here; the image fills its column and is
   cropped to one 3:2 ratio whatever its source (2:1 and 3:2 today), so the
   three bands' photos match. width/height on the <img> still reserve the box.
   __actions spaces a button row under the column's last block. */
.dpg-split__media{margin:0;}
.dpg-split__img{
  display:block;width:100%;height:auto;
  aspect-ratio:3 / 2;object-fit:cover;border-radius:var(--dpg-r-md);
}
.dpg-split__actions{margin-top:2.25rem;}
/* --flip: the first child moves to the END column on desktop only, so bands can
   alternate sides while every phone still reads DOM order (photo, then text).
   Gated to 901px+ because these are (0,2,0) and would otherwise outrank the
   (0,1,0) one-column collapse at 900px (field guide §3.1). */
@media (min-width:901px){
  .dpg-split--wide-end.dpg-split--flip{grid-template-columns:3fr 2fr;}
  .dpg-split--flip > :first-child{order:2;}
}

/* A section that HOSTS a .dpg-grid-bg overlay. Two rules, and both are needed:
   the band must be positioned for the overlay to resolve against it, and the
   container must be lifted, because .dpg-grid-bg is position:absolute;z-index:0
   and would otherwise paint OVER the section's static content. Lines take the
   root --dpg-grid-line, which is the on-dark cyan - correct for a dark band. */
.dpg-section--grid{position:relative;overflow:hidden;}
.dpg-section--grid > .dpg-container{position:relative;z-index:1;}

/* Light band only: relight the lines for a light surface and mask them so they
   read strongest at the top-right and are gone by the lower-left, as in the
   mockup. --tint is the light-context hook; add a new light band's class to
   these two selectors rather than restyling the grid inside that band. */
.dpg-section--tint.dpg-section--grid{--dpg-grid-line:var(--dpg-grid-line-light);}
.dpg-section--tint.dpg-section--grid .dpg-grid-bg{
  -webkit-mask-image:radial-gradient(100% 100% at 90% 0, #000 20%, transparent 80%);
          mask-image:radial-gradient(100% 100% at 90% 0, #000 20%, transparent 80%);
}

/* --blog: the banner texture for the blog index (client, 2026-09-22). DOTS.

   DECODED FROM THE CLIENT'S REFERENCE SVG, not guessed. That file draws 24
   horizontal paths, each stroke-width:2 with stroke-linecap:round and
   stroke-dasharray:"0.2 26" — a 0.2px dash with round caps on a 2px stroke is a
   2px ROUND DOT, repeating every 0.2+26 = 26.2px. The 24 y-values step by a
   constant 22.44px. Opacity 0.2. Hence r=1px on a 26.2 x 22.44 tile.

   VERIFIED AGAINST THE OTHER REFERENCE: this works out at 0.1069% ink, and the
   client's PNG measures 0.1077% — the same pattern, that file simply being a
   ~1.42x scale of it. Two independent references agreeing is why these numbers
   are not up for adjustment by eye.

   ⚠ THE DOTS ARE MEANT TO BE THIS SMALL AND THIS SPARSE — r=1 on a ~26px pitch
   is 0.53% of the band. Earlier builds used r=2 then r=1.5, and a 36px SQUARE
   GRID, all of which read wrong. If it looks absent, suspect the mask below
   before enlarging anything.

   ⚠⚠ THIS BLOCK MUST STAY BELOW THE BARE .dpg-grid-bg MASK RULE ABOVE. Both are
   (0,1,0), so source order alone decides which mask wins. An earlier build put
   this modifier ABOVE that rule and its mask override silently never applied —
   the texture kept the radial mask centred at 90% 0 and survived only in the
   top-right corner, which is exactly where a hero photograph sits. It looked
   like the dots were not rendering at all. Field guide §3.5: at equal
   specificity, verify the position in the file rather than assuming it.

   ⚠ THE MASK IS `none`, AND IT MUST STAY DECLARED — DO NOT DELETE THESE TWO
   LINES. An even dot field is what both references show and what the client
   asked for, but "even" here is NOT the same as "no mask property": the bare
   .dpg-grid-bg rule above sets a radial mask centred at 90% 0, and this modifier
   only escapes it by overriding it. Delete these two declarations and that
   radial mask applies again, clipping the dots to the top-right corner — which
   is precisely the bug that made them look absent through several builds. `none`
   is the override; removing it is not. */
.dpg-grid-bg--blog{
  --dpg-grid-line:var(--dpg-dot-line);
  background-image:radial-gradient(circle, var(--dpg-grid-line) 1px, transparent 1px);
  background-size:26.2px 22.44px;
  -webkit-mask-image:none;
          mask-image:none;
}

/* ----------------------------------------------------------------------------
   6. UTILITIES
---------------------------------------------------------------------------- */
.dpg-u-visually-hidden{position:absolute!important;width:1px;height:1px;padding:0;margin:-1px;
  overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0;}

/* ----------------------------------------------------------------------------
   7. RESPONSIVE
---------------------------------------------------------------------------- */

/* Hero photo: below this width the side-by-side split gets too tight for a
   640px text column beside it, so the photo stops being a right-hand panel and
   becomes a dimmed full-bleed backdrop instead. Deliberately its OWN breakpoint,
   earlier than the 900px one below: the nav hamburger and the card grids switch
   on their own schedule and should not be dragged forward with it. */
@media (max-width:1300px){
  .dpg-hero__media{
    width:100%;max-width:none;right:0;left:auto;
    opacity:.28;
    -webkit-mask-image:linear-gradient(180deg,transparent 0,#000 60%);
            mask-image:linear-gradient(180deg,transparent 0,#000 60%);
  }
}

@media (max-width:900px){
  /* One column now, so there is nothing to stay beside — and a panel stuck to
     the top of the viewport would cover the list it introduces. */
  .dpg-split__sticky{position:static;}
  .dpg-hero__content{max-width:none;}
  .dpg-header .dpg-header__actions{gap:.75rem;}   /* hamburger + CTA now share the bar */
  .dpg-proof-strip__grid{grid-template-columns:repeat(2,1fr);row-gap:2.25rem;}
  .dpg-proof-strip__stat:nth-child(odd){border-left:0;padding-inline-start:0;}
  .dpg-split{grid-template-columns:1fr;gap:2rem;}
  .dpg-split--wide-end{grid-template-columns:1fr;}
  .dpg-case__metrics{grid-template-columns:repeat(2,1fr);}
  .dpg-case__metric--featured{grid-row:auto;}

  .dpg-pillar-cards{grid-template-columns:1fr;}
  .dpg-card-grid{grid-template-columns:1fr;}
}
@media (max-width:760px){
  /* --row collapses to a single column. Every child is reset to auto placement,
     or the explicit coordinates above would hold them in a column that no
     longer exists. */
  .dpg-card--row{grid-template-columns:1fr;}
  .dpg-card--row .dpg-card__title,
  .dpg-card--row .dpg-card__media,
  .dpg-card--row .dpg-card__body,
  .dpg-card--row .dpg-card__link{grid-column:1;grid-row:auto;}
  .dpg-card--row .dpg-card__link{margin-top:.25rem;align-self:start;}
  .dpg-steps--split .dpg-steps__body{grid-template-columns:1fr;gap:.35rem;}
  .dpg-steps--grid{grid-template-columns:1fr;}
  .dpg-steps--grid .dpg-steps__item{border-left:0;padding-inline:0;padding-bottom:2rem;}
  .dpg-audience{grid-template-columns:1fr;}
  .dpg-reasons{grid-template-columns:1fr;}
  /* Below the .dpg-split collapse the triggers regain the full container width,
     so they pair up rather than running as one long single-file column. */
  .dpg-triggers{grid-template-columns:repeat(2,1fr);gap:2rem 2.5rem;}
  .dpg-feature-grid{grid-template-columns:1fr;gap:2rem;}
}
@media (max-width:560px){
  .dpg-case__metrics{grid-template-columns:1fr;}
  .dpg-case__metric--featured{grid-column:auto;}
  .dpg-triggers{grid-template-columns:1fr;}
  .dpg-proof-strip__grid{grid-template-columns:1fr;row-gap:1.75rem;}
  .dpg-proof-strip__stat{border-left:0;padding-inline:0;}
  .dpg-steps__item{gap:1rem;}
  .dpg-steps--split .dpg-steps__index{width:2.5rem;padding-top:.3rem;}
  .dpg-faq--numbered .dpg-faq__index{width:2.25rem;}
  .dpg-faq--numbered .dpg-faq__a{padding-left:0;}
  .dpg-steps--split .dpg-steps__item{padding-block:1.5rem;}
  /* The index gutter is dead space at phone width; the numeral sits above the
     title instead, which keeps the rows and the closing rule intact. */
  .dpg-scenarios__item{grid-template-columns:1fr;row-gap:.35rem;padding-block:1.35rem;}
  /* Same reasoning for the facts list: a 7.5rem label gutter leaves too little
     for the value on a phone, so the label sits above it and the rules stay. */
  .dpg-facts__row{grid-template-columns:1fr;row-gap:.3rem;padding-block:1.1rem;}
  /* Two columns of capability names is too tight on a phone. The 900px block
     already collapses the grid itself; this collapses the list inside it. */
  .dpg-feature-grid--single .dpg-feature-grid__list{grid-template-columns:1fr;}
}

@media (max-width:480px){
  /* Keep logo + hamburger + CTA on one row on small phones: the header logo at
     15px is 100px wide, the width the old one took here. */
  .dpg-header{--dpg-logo-h:15px;}
  .dpg-header .dpg-btn--dark{padding:.6rem .8rem;}
  .dpg-header__cta .dpg-btn__arrow{display:none;}
}
/* Phones 360px and wider have room for a bigger header logo (21px = 141px wide,
   client 2026-10-15); 15px stays for the 320px class, where any wider pushes
   the CTA onto two lines. */
@media (min-width:360px) and (max-width:480px){
  .dpg-header{--dpg-logo-h:21px;}
}

/* ----------------------------------------------------------------------------
   5.18 — 404 (page not found)
   ----------------------------------------------------------------------------
   Built from the client's mockup (assets/img/404-mock-up.png) at Step 10ae.
   Placed last in the file, with its own breakpoints, so a page that exists
   outside the build order cannot disturb the cascade of the pages that do.

   THE MEDIA MECHANIC IS THE HERO'S, not a new one: photo pinned to the right,
   width anchored to the CONTAINER rather than the viewport, faded out to the
   left with a mask so the text sits on clean ground. See §5.5 for the full
   derivation of why a flat width % detaches on wide screens. What differs is
   the ground — the hero fades into navy, this fades into a light band — so the
   two cannot share a rule, only the arithmetic.

   The two offsets are COMPONENT-scoped, not root tokens. The hero's live in
   §1.7 because two patterns share them; these have one user, and component
   layout is allowed to differ per component (field guide §1.4). */
.dpg-404{
  --dpg-404-media-start:26rem;  /* photo begins this far right of the container's left edge */
  --dpg-404-media-fade:22rem;   /* left fade length; fixed, NOT a % — same reason as the hero */
  position:relative;overflow:hidden;
  background:var(--dpg-slate-50);
  padding-block:clamp(3rem,7vw,6rem);
}
.dpg-404 picture{display:contents;}
.dpg-404__media{
  position:absolute;top:0;right:0;bottom:0;z-index:0;
  width:calc(100% - max(0px,(100% - var(--dpg-container))/2) - var(--dpg-404-media-start));
  max-width:none;
  height:100%;
  object-fit:cover;object-position:center right;
  -webkit-mask-image:linear-gradient(90deg,transparent 0,#000 var(--dpg-404-media-fade));
          mask-image:linear-gradient(90deg,transparent 0,#000 var(--dpg-404-media-fade));
}
.dpg-404__inner{position:relative;z-index:1;}
.dpg-404__content{max-width:36rem;}

/* THE NUMERALS ARE ARTWORK, NOT A HEADING — they carry aria-hidden and the <h1>
   below says "Page not found" in words. Weight 500 is deliberate and it has a
   side effect worth knowing: it is the only caller of Raleway 500
   (raleway-v37-latin-500.woff2 since the 2026-10-05 font switch; Space Grotesk
   500 before it), so that file is downloaded ONLY on this page. 700 would look nothing like
   the mockup, which is why the lighter cut earns its place. */
.dpg-404__numerals{
  display:flex;align-items:center;
  margin:.5rem 0 1.25rem;
  font-family:var(--dpg-font-heading);
  font-size:var(--dpg-fs-display);
  font-weight:500;line-height:1;letter-spacing:-.015em;
  color:var(--dpg-heading);
}
/* Sized in em so the compass tracks the numerals at every clamp step. .78em is
   a little above cap height, which is what makes it read as the same optical
   size as the 4s rather than measurably smaller. */
.dpg-404__compass{
  flex:none;width:.78em;height:.78em;
  margin-inline:.05em;
  color:var(--dpg-heading);
}

/* Build spec §9.2 compliant: --dpg-fs-h1 at weight 700. The mockup tracks this
   line wider than .12em; at 56px that would overrun the content column, so the
   tracking is the value that fits and the SIZE is the spec's. */
.dpg-404__title{
  margin:0;
  font-family:var(--dpg-font-heading);
  font-size:var(--dpg-fs-h1);font-weight:700;
  line-height:var(--dpg-lh-tight);letter-spacing:.12em;
  text-transform:uppercase;
  color:var(--dpg-heading);
}
.dpg-404__lead{
  margin:1.75rem 0 0;
  font-family:var(--dpg-font-heading);
  font-size:var(--dpg-fs-h3);font-weight:700;
  line-height:var(--dpg-lh-heading);
  color:var(--dpg-heading);
}
.dpg-404 .dpg-prose{margin-top:.9rem;max-width:32rem;}

/* Same 1rem gap as .dpg-hero__actions — this is now the same pair of buttons in
   the same relationship (solid primary + bordered secondary), so it should sit
   the same way. Client, 2026-09-18.

   THE SECONDARY IS .dpg-btn--outline, not a bespoke label. The mockup draws a
   ruled text link there and the first build followed it; the client asked for
   the homepage hero's "Talk to us" treatment instead — a real bordered button.
   That variant already existed for light grounds and had simply never been used,
   so this needed no new component: --outline IS --ghost for a light band.
   The bespoke .dpg-404__alt rules it replaced were deleted rather than left
   dormant — nothing else used them. */
.dpg-404__actions{
  display:flex;flex-wrap:wrap;align-items:center;
  gap:1rem;
  margin-top:2.25rem;
}

@media (max-width:1100px){
  .dpg-404{--dpg-404-media-start:20rem;--dpg-404-media-fade:16rem;}
}
/* One column below 900: the photo stops being a neighbour and becomes texture
   behind the text, faded from the top — the same treatment the hero takes at
   this width. Opacity is lower here because the ground is LIGHT and the text on
   it is dark, which is the harder direction for legibility. */
@media (max-width:900px){
  .dpg-404__media{
    width:100%;left:auto;right:0;
    opacity:.22;
    /* `center right` is the desktop crop, where the photo sits beside the text
       and the desk is the subject. Full-bleed behind the text it should be
       centred, or a narrow phone crops to the edge of the frame. */
    object-position:center;
    -webkit-mask-image:linear-gradient(180deg,transparent 0,#000 55%);
            mask-image:linear-gradient(180deg,transparent 0,#000 55%);
  }
  .dpg-404__content{max-width:none;}
}
@media (max-width:560px){
  /* Tracking only. The actions deliberately carry NO mobile rule: an earlier
     build forced them full-width and centred, which was written when the
     secondary was a bespoke ruled label and the two needed help to line up.
     They are both .dpg-btn now, so they behave exactly like the homepage hero's
     pair — auto width, left aligned, wrapping to a stack when they no longer
     fit side by side. Matching the hero is the point (client, 2026-09-18), and
     the hero has no mobile button rule either. */
  .dpg-404__title{letter-spacing:.08em;}
}

/* ----------------------------------------------------------------------------
   5.19 — (retired at Step 19i) The GoHighLevel iframe's .dpg-form-embed rules
   lived here. The Contact page now has its own form, styled in contact.css
   (loaded on that page only).
   ---------------------------------------------------------------------------- */

/* ----------------------------------------------------------------------------
   5.20 — Post card (.dpg-card--post) and its grid — MOVED FROM blog.css §6
---------------------------------------------------------------------------- */
/* Built for the blog index at Step 15b (client mockup,
   assets/img/blog-archive-card.png) and moved here at Step 15i, when the
   homepage's "Latest insights" section (patterns/home-insights.php) became its
   first caller outside the blog. Build spec §7: a component with callers on
   more than one page type belongs in the global library, not in a page file.
   Moved verbatim; only this header and the next paragraph's first lines changed. */
/* A MODIFIER OF THE ONE CARD, not a new card (build spec §9.6): .dpg-card--post
   keeps .dpg-card's column flex, __title, __body and __media, and replaces the
   frame. Callers: the blog's list pattern and the homepage's Latest insights
   section (see the header above for why it now lives in this file).

   COLOURS WERE SAMPLED, and every one landed on a token (field guide §0.2):
   frame and rules #060606 -> --dpg-heading (the mockup's near-black is the
   site's darkest light-ground ink); tag and button #bdfa41-#bffb47 ->
   --dpg-lime, the brand lime, ~10/255 off in green and ~35 in blue, kept on the
   token rather than matched; dividers #9b9eaa -> --dpg-slate-400; dots #c9c9cb
   -> --dpg-slate-300; the slanted panel #eff0f2 -> --dpg-slate-100 (its first
   caller since Step 02); date #5d697e -> --dpg-slate-500, 4.76:1 on white.

   TEXT IS SET ON THE SITE'S TYPE TOKENS, NOT SCALED FROM THE MOCKUP. The mockup
   is one card drawn ~1200px wide; at a three-up card's ~384px its excerpt would
   scale to about 9px. So the GEOMETRY — the cut corner, the panel, the dot grid,
   the text column's right margin — is in cqi of the card and matches the
   mockup's proportions, while the words stay readable.

   THREE KNOBS, one each, so the frame's parts cannot drift apart:
   --dpg-post-card-line  frame colour: border, image rule and the cut's diagonal
   --dpg-post-card-rule  their width. 1.5px is the mockup's 4px at its own width,
                         scaled to a three-up card (snaps to 1px at 1x, 3 device
                         px at 2x)
   --dpg-post-card-cut   the corner cut, ~16cqi at 384px as in the mockup. It
                         is a rem length and not cqi because clip-path on the card
                         cannot read the card's own container units — an element
                         is never its own query container.
   Two classes on the frame rule, because it overrides .dpg-card's own padding,
   border and radius from a different file (field guide §3.5).
   Line and rule are declared on the filter-and-search row (blog.css §7) and the
   page numbers (blog.css §9) too, whose buttons are this frame in miniature — one
   declaration, so they cannot drift. */
.dpg-card--post,
.dpg-blog-tools,
.dpg-blog-pages{
  --dpg-post-card-line:var(--dpg-heading);
  --dpg-post-card-rule:1.5px;
}
.dpg-card.dpg-card--post{
  --dpg-post-card-cut:4rem;
  position:relative;container-type:inline-size;
  padding:0;gap:0;border-radius:0;
  border:var(--dpg-post-card-rule) solid var(--dpg-post-card-line);
  clip-path:polygon(0 0,100% 0,100% calc(100% - var(--dpg-post-card-cut)),calc(100% - var(--dpg-post-card-cut)) 100%,0 100%);
}
/* The cut's own edge. clip-path removes the corner AND the two borders that ran
   into it, so the diagonal is drawn back in: a square the size of the cut, on
   the border-box corner, whose gradient paints a band exactly --rule thick just
   inside the anti-diagonal — the line clip-path cuts along. The outer half of the
   square is clipped away, so the band needs no second stop on that side. */
.dpg-card--post::after{
  content:"";position:absolute;pointer-events:none;
  right:calc(-1 * var(--dpg-post-card-rule));bottom:calc(-1 * var(--dpg-post-card-rule));
  width:var(--dpg-post-card-cut);height:var(--dpg-post-card-cut);
  background:linear-gradient(to top left,transparent 50%,var(--dpg-post-card-line) 50%,var(--dpg-post-card-line) calc(50% + var(--dpg-post-card-rule)),transparent calc(50% + var(--dpg-post-card-rule)));
}

/* THE IMAGE SLOT IS 13:6, the mockup's (1202 x 556), NOT the 3:2 of the images
   themselves. The automation's featured images are 1536 x 1024 and the same
   file is the single post's banner, so object-fit crops ~30% of the height here,
   centred. If that loses too much of a picture, aspect-ratio is the one value to
   change — 3/2 shows the whole frame. The slot paints --dpg-navy under the
   image, which is also the whole of the no-image panel (build spec §6.3). */
.dpg-card--post .dpg-card__media{
  aspect-ratio:13 / 6;overflow:hidden;background:var(--dpg-navy);
  border-bottom:var(--dpg-post-card-rule) solid var(--dpg-post-card-line);
}
.dpg-card--post .dpg-card__img{width:100%;height:100%;object-fit:cover;border-radius:0;}

/* THE TEXT COLUMN STOPS AT ~79% of the card, as in the mockup: its right 21cqi
   is where the dot grid and the slanted panel sit, and text never runs under
   them. isolation:isolate is load-bearing — both decorations are z-index:-1 and
   must paint above the card's white and below the words, which only happens if
   this element forms the stacking context (field guide §4.8). */
.dpg-card--post .dpg-card__content{
  position:relative;isolation:isolate;flex:1 1 auto;
  display:flex;flex-direction:column;
  padding:1.25rem 21cqi 1.5rem 1.25rem;
}
/* Dot grid: 6 x 8 at the mockup's spacing (2.5cqi), cut away at the lower
   right along a 45° line and then straight down, exactly where the mockup drops
   its dots. Positions are the mockup's, measured off the PNG, in cqi. */
.dpg-card--post .dpg-card__content::before{
  content:"";position:absolute;z-index:-1;pointer-events:none;
  top:1.9cqi;right:2.2cqi;width:15cqi;height:20cqi;
  background:radial-gradient(circle,var(--dpg-slate-300) .29cqi,transparent calc(.29cqi + .5px)) 0 0 / 2.5cqi 2.5cqi;
  clip-path:polygon(0 0,100% 0,100% 45%,45.6% 85.8%,45.6% 100%,0 100%);
}
/* Slanted panel: a parallelogram against the right border whose edges run at
   45°, parallel to the cut. Its bottom-right corner sits 4.6cqi above where the
   cut begins, as in the mockup — hence the calc against the cut, not the floor.
   That corner is 40.9% of the box (9.16cqi) above the box's own bottom, so the
   box sits at cut + 4.6cqi - 9.16cqi. */
.dpg-card--post .dpg-card__content::after{
  content:"";position:absolute;z-index:-1;pointer-events:none;
  right:0;bottom:calc(var(--dpg-post-card-cut) - 4.56cqi);width:9.4cqi;height:22.4cqi;
  background:var(--dpg-slate-100);
  clip-path:polygon(0 40.9%,100% 0,100% 59.1%,0 100%);
}

/* Byline: author | date | category. At a three-up card's width the three
   rarely fit on one line, so it wraps — and a divider must never lead a line.
   Each divider hangs half a gap to the LEFT of its item; overflow:hidden clips
   whichever one lands at the start of a line, because that one falls outside
   the box. The 6px of padding (cancelled by the margin) is room for the focus
   ring, which is 2px at a 3px offset and would otherwise be clipped too. */
.dpg-card--post .dpg-card__byline{
  display:flex;flex-wrap:wrap;align-items:center;column-gap:1.5rem;row-gap:.6rem;
  overflow:hidden;padding:6px;margin:-6px;
  font-size:var(--dpg-fs-small);line-height:1.2;
}
.dpg-card__author{font-weight:600;color:var(--dpg-heading);}
.dpg-card__date{
  position:relative;display:inline-flex;align-items:center;gap:.45rem;
  color:var(--dpg-slate-500);
}
.dpg-card__date svg{width:1rem;height:1rem;flex:none;color:var(--dpg-slate-600);}
.dpg-card__date::after,
.dpg-card__tag::after{
  content:"";position:absolute;left:calc(-.75rem - .5px);top:50%;
  width:1px;height:1.75rem;transform:translateY(-50%);background:var(--dpg-slate-400);
}
/* The category tag and the button are both LIME SHAPES WITH A SLANTED EDGE,
   and both draw the shape on ::before rather than clipping themselves: a
   clip-path on the link would clip its own focus ring. Same stacking-context
   rule as the content column above. The tag leans "/ /", the button's single
   slant "\" — as drawn. */
.dpg-card__tag{
  --dpg-post-tag-slant:1.15rem;
  position:relative;isolation:isolate;display:inline-flex;align-items:center;gap:.4rem;
  padding:.35rem calc(var(--dpg-post-tag-slant) + .55rem) .35rem calc(var(--dpg-post-tag-slant) + .25rem);
  font-weight:600;color:var(--dpg-heading);text-decoration:none;
}
.dpg-card__tag::before{
  content:"";position:absolute;inset:0;z-index:-1;background:var(--dpg-lime);
  clip-path:polygon(var(--dpg-post-tag-slant) 0,100% 0,calc(100% - var(--dpg-post-tag-slant)) 100%,0 100%);
  transition:background-color var(--dpg-t-fast) var(--dpg-ease);
}
.dpg-card__tag:hover::before{background:var(--dpg-lime-hover);}
.dpg-card__tag svg{width:.95rem;height:.95rem;flex:none;}

.dpg-card--post .dpg-card__title{margin-top:1rem;letter-spacing:-.02em;}
/* Excerpts differ in length, so the body is clamped at three lines with an
   ellipsis, and it keeps .dpg-card's flex:1 1 auto — so it also absorbs the
   spare height of a shorter card. Grid items stretch to the tallest card in the
   row, so every "Read more" in a row lands on the same line. */
.dpg-card--post .dpg-card__body{
  margin-top:.6rem;
  display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:3;line-clamp:3;overflow:hidden;
}
.dpg-card__more{
  --dpg-post-more-slant:1.4rem;
  position:relative;isolation:isolate;align-self:flex-start;
  display:inline-flex;align-items:center;gap:1.9rem;margin-top:1.25rem;
  padding:.6rem calc(var(--dpg-post-more-slant) + 1.1rem) .6rem 1.2rem;
  font-size:var(--dpg-fs-small);font-weight:600;line-height:1.2;
  color:var(--dpg-heading);text-decoration:none;
}
.dpg-card__more::before{
  content:"";position:absolute;inset:0;z-index:-1;background:var(--dpg-lime);
  clip-path:polygon(0 0,calc(100% - var(--dpg-post-more-slant)) 0,100% 100%,0 100%);
  transition:background-color var(--dpg-t-fast) var(--dpg-ease);
}
.dpg-card__more svg{
  width:1.15rem;height:1rem;flex:none;
  transition:transform var(--dpg-t-fast) var(--dpg-ease);
}
.dpg-card__more:hover::before{background:var(--dpg-lime-hover);}
.dpg-card__more:hover svg{transform:translateX(3px);}

/* THE GRID: three across from 1100px, two from 640px, one below. Two classes
   because global.css's own .dpg-card-grid collapse inside its 900px query is
   one class, and a modifier should not depend on which file loads last (field
   guide §3.5). If these breakpoints move, dpg_post_card()'s sizes string (inc/blog.php) moves
   with them. */
.dpg-card-grid.dpg-card-grid--posts{grid-template-columns:minmax(0,1fr);gap:2rem;margin-top:0;}
@media (min-width:640px){
  .dpg-card-grid.dpg-card-grid--posts{grid-template-columns:repeat(2,minmax(0,1fr));}
}
@media (min-width:1100px){
  .dpg-card-grid.dpg-card-grid--posts{grid-template-columns:repeat(3,minmax(0,1fr));}
}
/* HOMEPAGE LATEST INSIGHTS SHOWS TWO ON A TABLET (Step 15i). It always holds
   exactly three cards, and the grid above is two columns from 640px to 1099px,
   so the third would sit alone on a second row. That range hides it; a phone
   stacks all three and a desktop shows three across. The blog index is not
   affected — this is scoped to the homepage grid's own class. */
@media (min-width:640px) and (max-width:1099.98px){
  .dpg-home-insights__grid > :nth-child(3){display:none;}
}
