/* ==========================================================================
   Dot Map — layout
   --------------------------------------------------------------------------
   Cascade position 3 of 5:  tokens -> base -> LAYOUT -> components -> pages

   Contents
     0. Breakpoint contract (read this first)
     1. Container
     2. Section rhythm
     3. Grid + stack utilities
     4. Colour-band + overflow utilities
     5. Foundation primitives — image slot, media
     6. Site header + primary nav + mobile nav toggle
     7. Site footer

   ========================================================================== */


/* ==========================================================================
   0. BREAKPOINT CONTRACT — binding on every agent
   --------------------------------------------------------------------------
   The design kit is fixed-width desktop (minWidth: 1280) with ZERO
   breakpoints, no mobile nav and no fluid type. The entire responsive layer
   is net-new and defined here.

   MOBILE-FIRST, min-width ONLY. There is not a single max-width media query
   in this theme, and there must not be one. Base styles are the phone layout;
   each breakpoint adds, never subtracts.

     (base)            0–639px    Phone. Single column. Nav collapsed behind a
                                  toggle. Reference width for QA: 390px.
     @media (min-width: 640px)    Large phone / small tablet. Two-up grids
                                  become possible. Nav still collapsed.
     @media (min-width: 900px)    THE LAYOUT BREAKPOINT. Horizontal nav
                                  replaces the toggle. Multi-column page
                                  layouts and the row-form footer switch on.
     @media (min-width: 1200px)   Container reaches --container-max. Largest
                                  section rhythm (128px). Reference width for
                                  QA: 1280px and 1440px.

   Write breakpoints in that ascending order inside each component block.
   Do not introduce a fifth breakpoint. Do not use max-width queries.
   Feature queries (prefers-reduced-motion, forced-colors) are exempt.

   CLASS NAMING — binding on every agent
     Prefix        every class is `dm-`. No exceptions, no unprefixed classes.
     Block         .dm-card
     Element       .dm-card__title         (double underscore)
     Modifier      .dm-card--elevated      (double hyphen)
     State         .is-open, .is-current, .is-active, .has-error
                   State classes are toggled by JavaScript and are NEVER
                   styled on their own — always `.dm-nav.is-open`, so the
                   state cannot leak across components.
     Utility       .dm-container, .dm-section, .dm-grid, .dm-stack, .dm-on-dark

   ROOT SCOPE — `.dm-page` — binding on every agent
   --------------------------------------------------------------------------
   Elementor Pro stays installed and active alongside this theme. It injects
   broad global resets plus an `.elementor-kit-{N}` stylesheet carrying its own
   body/heading/link typography. Those rules sit at specificity (0,1,1) and
   will beat a bare element or single-class selector.

   So: `body_class` puts `dm-page` on every <body> (see dotmap_body_class() in
   inc/setup.php), and EVERY visual rule in this theme is scoped under it.

     base.css       `body.dm-page h1 { … }`        (0,1,2)
     layout.css     `.dm-page .dm-container { … }` (0,2,0)
     components.css `.dm-page .dm-card { … }`      (0,2,0)  ← REQUIRED
     pages.css      `.dm-page-home .dm-hero { … }` (0,2,0)  ← already satisfied,
                    because dm-page-home is a second class on the same <body>

   `.dm-page` is on <body>, so scoping changes SPECIFICITY ONLY — never which
   elements a rule reaches. The reach is identical; the rule simply wins.

   This is deliberately calibrated, not maximal. Elementor's own widget CSS
   (`.elementor-widget-text-editor p`, 0,2,1) still outranks our base rules
   (0,1,2), which is the correct outcome: inside an Elementor-built block,
   Elementor governs. Outside it, we do.

   Two things do NOT get the scope, on purpose:
     - the structural reset in base.css section 2 (box-sizing, margin zeroing,
       [hidden]) — it must stay low-specificity so Elementor content can win;
     - @font-face, and the prefers-reduced-motion safety net.

   The print order is also stacked in our favour: inc/enqueue.php hooks
   wp_enqueue_scripts at priority 20, after Elementor's default-10 enqueues,
   so an exact specificity TIE resolves to us. Never dequeue Elementor's or
   Formidable's stylesheets — override them.

   SPECIFICITY
     - No IDs in selectors.
     - No `!important`. The theme contains exactly FOUR documented uses, each
       commented at the point of use:
         1. base.css  [hidden]
         2. base.css  .screen-reader-text positioning
         3. base.css  the prefers-reduced-motion safety net
         4. layout.css the motion pause rule (see "ONE RULE, NOT A LIST")
       All four override styles the theme does not control -- user-agent
       defaults, author styles from plugins, or in case 4 an `animation`
       shorthand that resets play-state. If you believe you need a fifth,
       raise it: the scope class plus the priority-20 print order should make
       it unnecessary.

       This count was wrong for one release -- it still said "three" after the
       fourth shipped. A comment asserting a number is a claim that has to be
       re-checked, not decoration.
     - Beyond the mandatory `.dm-page` scope, keep selectors shallow.

   CASING
     Sentence case is law, in markup AND in CSS. No `text-transform:
     uppercase` anywhere — including mono eyebrows, which keep their
     letterspacing but not the caps.
   ========================================================================== */


/* ==========================================================================
   1. Container
   --------------------------------------------------------------------------
   One horizontal rhythm for the entire site. The site header and footer use
   it too, so chrome and content share a left edge at every width.
   ========================================================================== */

.dm-page .dm-container {
	width: 100%;
	max-width: var(--container-max);
	margin-inline: auto;
	padding-inline: var(--gutter);
}

/* Prose column — comfortable measure for long-form copy. */
.dm-page .dm-container--narrow {
	max-width: var(--container-narrow);
}

/* Edge-to-edge colour band whose *contents* still align to the container.
   Put the band class on the section and .dm-container on an inner div. */
.dm-page .dm-container--flush {
	max-width: none;
	padding-inline: 0;
}


/* ==========================================================================
   2. Section rhythm
   --------------------------------------------------------------------------
   The design system specifies 96–128px vertical spacing between major
   marketing blocks. That is a DESKTOP figure: 96px of dead space above and
   below every block on a 390px screen is roughly a third of the viewport, so
   the scale steps down on small screens and reaches the specified range at
   900px and above.

   All values are spacing tokens; nothing here is a magic number.

     .dm-section          64  ->  80 @640  ->  96 @900             (default)
     .dm-section--lg      80  ->  96 @640  ->  96 @900 -> 128 @1200
     .dm-section--tight   48  ->  48 @640  ->  64 @900
   ========================================================================== */

.dm-page .dm-section {
	padding-block: var(--space-16); /* 64px */
}

.dm-page .dm-section--lg {
	padding-block: var(--space-20); /* 80px */
}

.dm-page .dm-section--tight {
	padding-block: var(--space-12); /* 48px */
}

/* Remove rhythm on one side when two bands of the same colour meet. */
.dm-page .dm-section--flush-top {
	padding-block-start: 0;
}

.dm-page .dm-section--flush-bottom {
	padding-block-end: 0;
}

@media (min-width: 640px) {
	.dm-page .dm-section {
		padding-block: var(--space-20); /* 80px */
	}

	.dm-page .dm-section--lg {
		padding-block: var(--space-24); /* 96px */
	}
}

@media (min-width: 900px) {
	.dm-page .dm-section {
		padding-block: var(--space-24); /* 96px — bottom of the brand range */
	}

	.dm-page .dm-section--tight {
		padding-block: var(--space-16); /* 64px */
	}
}

@media (min-width: 1200px) {
	.dm-page .dm-section--lg {
		padding-block: var(--space-32); /* 128px — top of the brand range */
	}
}


/* ==========================================================================
   3. Grid + stack utilities
   --------------------------------------------------------------------------
   Every grid is one column on a phone and gains columns upward. Use these
   rather than writing a bespoke grid per page — a shared scaffold is the only
   reason three page agents can work in parallel without the gutters drifting.
   ========================================================================== */

.dm-page .dm-grid {
	display: grid;
	grid-template-columns: 1fr;
	gap: var(--space-6); /* 24px — matches the card padding token */
}

.dm-page .dm-grid--gap-lg {
	gap: var(--space-8); /* 32px */
}

.dm-page .dm-grid--gap-sm {
	gap: var(--space-4); /* 16px */
}

@media (min-width: 640px) {
	/* Two-up as soon as there is room for two readable cards. */
	.dm-page .dm-grid--2,
	.dm-page .dm-grid--3,
	.dm-page .dm-grid--4 {
		grid-template-columns: repeat(2, minmax(0, 1fr));
	}
}

@media (min-width: 900px) {
	.dm-page .dm-grid--3 {
		grid-template-columns: repeat(3, minmax(0, 1fr));
	}

	.dm-page .dm-grid--4 {
		grid-template-columns: repeat(4, minmax(0, 1fr));
	}
}

/* Asymmetric split for text-beside-media blocks. Stacked until 900. */
@media (min-width: 900px) {
	.dm-page .dm-grid--split {
		grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
		gap: var(--space-12);
		align-items: center;
	}

	.dm-page .dm-grid--split-wide {
		grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
		gap: var(--space-12);
		align-items: center;
	}
}

/*
 * Vertical rhythm inside a block. `> * + *` rather than a margin on every
 * child means no first/last-child margin cleanup is ever needed.
 */
.dm-page .dm-stack > * + * {
	margin-block-start: var(--space-4); /* 16px */
}

.dm-page .dm-stack--sm > * + * {
	margin-block-start: var(--space-2); /* 8px */
}

.dm-page .dm-stack--lg > * + * {
	margin-block-start: var(--space-6); /* 24px */
}

.dm-page .dm-stack--xl > * + * {
	margin-block-start: var(--space-10); /* 40px */
}


/* ==========================================================================
   4. Colour-band + overflow utilities
   ========================================================================== */

/*
 * Dark band. Flat colour field only — no gradient, no blur, no texture.
 * Also switches the focus ring to the light end of the ink scale (see the
 * .dm-on-dark rule in base.css) so focus stays visible on navy.
 */
.dm-page .dm-on-dark {
	background-color: var(--bg-inverse);
	color: var(--text-on-dark);
}

.dm-page .dm-on-dark h1,
.dm-page .dm-on-dark h2,
.dm-page .dm-on-dark h3,
.dm-page .dm-on-dark h4,
.dm-page .dm-on-dark h5,
.dm-page .dm-on-dark h6 {
	color: var(--ink-0);
}

.dm-page .dm-on-dark a {
	color: var(--ink-0);
}

.dm-page .dm-on-sunken {
	background-color: var(--bg-sunken);
}

.dm-page .dm-on-surface {
	background-color: var(--bg-surface);
}

/*
 * Anything too wide for a 390px screen — a data table, a code block, a wide
 * diagram — goes inside one of these. The page body itself must never scroll
 * horizontally.
 */
.dm-page .dm-scroll-x {
	overflow-x: auto;
	max-width: 100%;
	-webkit-overflow-scrolling: touch;
}

/*
 * Mono eyebrow / kicker.
 *
 * KIT CONFLICT, RESOLVED AGAINST THE KIT. The Type — Mono specimen card
 * (guidelines/type-mono.card.html) sets `text-transform: uppercase` on the
 * eyebrow. The design system README and the build brief both state the rule
 * as "sentence case throughout — no ALL CAPS", and the brief's casing
 * decision settles it explicitly: eyebrows keep the --tracking-widest
 * letterspacing but drop the caps. There is therefore no text-transform here,
 * and there must not be one anywhere in this theme.
 */
/*
 * A11Y (1.4.3, Agent 8): the default colour was --text-muted, which measures
 * 3.28:1 on --bg-sunken and 3.83:1 on --bg-surface — both short of 4.5:1 for
 * 13px text. Today both call sites (hero.php and page-hero.php) pair this class
 * with a component class that recolours it to teal-300 on navy at 9.14:1, so
 * nothing on screen changes; this is the trap removed, not a repaint. Anyone
 * adding a bare .dm-eyebrow on a light band would otherwise ship a failure.
 */
.dm-page .dm-eyebrow {
	display: block;
	color: var(--text-secondary);
	font-family: var(--font-mono);
	font-size: var(--text-mono-label);
	font-weight: var(--weight-medium);
	letter-spacing: var(--tracking-widest);
}


/* ==========================================================================
   5. Foundation primitives
   --------------------------------------------------------------------------
   Rendered by inc/helpers.php. OWNED BY THE FOUNDATION LANE — do not
   redefine these in components.css or pages.css. Extend them with a modifier
   class in your own file if you need a variant.
   ========================================================================== */

/*
 * .dm-image-slot — the styled placeholder, the PHP equivalent of the kit's
 * <image-slot> empty state. Reserves the real image's box so nothing reflows
 * when the asset arrives, and carries a visible label naming what is missing.
 * Never a blank box, never a stock photo.
 *
 * --dm-slot-ratio is set per call site by dotmap_image_slot(); the value is
 * whitelisted in PHP, not merely escaped.
 */
.dm-page .dm-image-slot {
	position: relative;
	width: 100%;
	min-height: var(--space-24); /* 96px — never collapses */
	aspect-ratio: var(--dm-slot-ratio, 3 / 2);
	border-radius: var(--radius-lg);
	background-color: var(--bg-sunken);
	overflow: hidden;
}

.dm-page .dm-image-slot__frame {
	position: absolute;
	inset: 0;
	display: flex;
	align-items: center;
	justify-content: center;
	padding: var(--space-4);
	border: var(--border-width-hairline) dashed var(--border-strong);
	border-radius: inherit;
}

/*
 * A11Y (1.4.3, Agent 8) — CONFIRMED FAILURE, FIXED.
 * --text-muted (--ink-400 #7d858c) on --bg-sunken (#eef0f1) measured 3.28:1 in
 * the browser at 12px/500. That is real visible text ("Project screenshot
 * needed", "Team photo") and it fails the 4.5:1 minimum. It rendered on Work,
 * About, the case-study archive, the tag archive and every case-study single —
 * five surfaces. --text-secondary (--ink-600) measures 7.39:1 on the same
 * ground, which is the figure pages.css §WORK already cites for the tag chip
 * sitting on that surface, so the two now agree.
 */
.dm-page .dm-image-slot__label {
	color: var(--text-secondary);
	font-family: var(--font-mono);
	font-size: var(--text-caption);
	font-weight: var(--weight-medium);
	letter-spacing: var(--tracking-wide);
	line-height: var(--leading-snug);
	text-align: center;
}

/* A slot sitting flush at the top of a card must not round its bottom edge. */
.dm-page .dm-image-slot--flush-top {
	border-radius: var(--radius-lg) var(--radius-lg) 0 0;
}

/* Real image rendered by dotmap_thumbnail_or_slot(). */
.dm-page .dm-media__img {
	display: block;
	width: 100%;
	height: auto;
	background-color: var(--bg-sunken); /* Shown while the image decodes. */
}


/* ==========================================================================
   6. Site header
   --------------------------------------------------------------------------
   Header.jsx spec: flex row, space-between, 20px/48px padding, 1px
   --border-default bottom rule, --bg-surface background. Logo left, linked
   home. Nav right at 14px/600, gap 28, current item in --brand-primary and
   the rest in --text-secondary. Home is the logo, not a nav item.

   NET-NEW (the kit has none of this):
     - the entire sub-900px layout,
     - the disclosure toggle and panel,
     - focus styles,
     - touch-sized hit areas.

   MOTION LAW. The panel does not slide, fade, grow or reorder. It is shown
   and hidden outright. The only transitions anywhere in this block are
   colour and background-color at --duration-fast, which is exactly what the
   brand permits. Do not add a height/transform/opacity transition here.

   NO-JAVASCRIPT BEHAVIOUR. Below 900px the panel is display:none and the
   toggle button carries the `hidden` attribute until nav.js removes it. With
   JavaScript off, a phone user therefore sees no dead control and no broken
   menu — they navigate from the footer nav, which always renders all five
   links for exactly this reason. Because the collapsed state is the CSS
   default rather than something JavaScript applies, there is no
   flash-of-open-menu and no inline critical script is required.

   The header is deliberately NOT sticky, matching the kit. If it is ever made
   sticky, add `position: sticky; top: 0; z-index: 100;` here AND raise
   `scroll-padding-top` in base.css to the header's height, or in-page anchors
   will land underneath it.
   ========================================================================== */

.dm-page .dm-site-header {
	position: relative;
	z-index: 50;
	border-bottom: var(--border-width-hairline) solid var(--border-default);
	background-color: var(--bg-surface);
}

.dm-page .dm-site-header__inner {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	justify-content: space-between;
	gap: var(--space-4);
	padding-block: var(--space-4); /* 16px on a phone */
}

.dm-page .dm-site-header__brand {
	display: flex;
	align-items: center;
	/* Sits above the wrapped nav panel in source order and on screen. */
	flex: 0 0 auto;
}

@media (min-width: 900px) {
	.dm-page .dm-site-header__inner {
		flex-wrap: nowrap;
		padding-block: var(--space-5); /* 20px — the kit's figure */
	}
}


/* --- Logo ---------------------------------------------------------------- */

/*
 * min-height is WCAG 2.2 AA 2.5.8 Target Size (Agent 8). The footer wordmark is
 * 23px tall (the 76x23 box below), so the link that carries it measured
 * 76 x 23 in the browser — one pixel under the 24x24 minimum on every page of
 * the site. The spacing exception arguably covers it, since nothing else is
 * within 24px, but "arguably exempt" is a weak answer in a public-sector
 * procurement review and the fix costs one pixel. The header logo is already
 * 28/33px tall and is unaffected.
 */
.dm-page .dm-logo-link {
	display: inline-flex;
	align-items: center;
	min-height: var(--space-6); /* 24px — WCAG 2.2 AA 2.5.8 */
	text-decoration: none;
	border-radius: var(--radius-md);
}

/*
 * BUNDLED WORDMARK CROP — calibrated to assets/img/dot-map-logo.png.
 *
 * That file is 1080x600 with a transparent background, but the ink only
 * occupies x 136–962 / y 154–426. Roughly 45% of the canvas height and 20%
 * of its width are empty padding. Sizing the <img> by height alone — which is
 * what the kit does, at height:34 — renders the wordmark at about 15px tall
 * inside a 34px box, far smaller than it appears in the source file.
 *
 * object-fit: cover on an explicitly-sized box crops the vertical dead space
 * away. The ink is centred within 2% of the canvas centre vertically, so a
 * centred crop keeps the whole wordmark. At 108x33 the scale factor is 0.1
 * and the wordmark renders ~83x27 — a correctly-weighted header logo.
 *
 * THIS CROP IS SPECIFIC TO THIS ASSET. Replace the file with a properly
 * trimmed wordmark and this rule must be deleted, not re-tuned. A trimmed
 * asset is tracked as a content gap.
 */
.dm-page .dm-logo--bundled {
	/*
	 * The white wordmark, supplied by the operator: 800x300 RGBA, pure white
	 * ink on real alpha, ink bounded x 38-670 / y 70-278.
	 *
	 * No `filter` and no `object-fit: cover` crop. Both existed to rescue the
	 * previous asset, which was charcoal artwork with ~45% empty padding and
	 * needed knocking out AND cropping to be legible on navy. This one is
	 * already the right colour and mostly trimmed, so the CSS gets out of the
	 * way -- a filter on artwork that is already white is a no-op that only
	 * confuses the next reader.
	 */
	object-fit: contain;
	object-position: left center;
}

.dm-page .dm-logo--bundled.dm-logo--header {
	/* 800 x 300 = 2.667:1. The old box was cut for a 1.8:1 asset. */
	width: 75px;
	height: 28px;
}

.dm-page .dm-logo--bundled.dm-logo--footer {
	width: 61px;
	height: 23px;
}

/*
 * A Customizer-uploaded logo has unknown proportions, so it is never cropped.
 * contain letterboxes it inside the same height budget instead.
 */
.dm-page .dm-logo--custom {
	width: auto;
	max-width: 200px;
	height: 34px;
	object-fit: contain;
	object-position: left center;
}

.dm-page .dm-logo--custom.dm-logo--footer {
	height: 24px;
	/*
	 * NO knockout filter here, deliberately.
	 *
	 * The filter on .dm-logo--footer above is safe ONLY because the bundled
	 * wordmark was verified to have a real alpha channel (corner pixel a=0), so
	 * just the letterforms invert. A Customizer upload has unknown art: the logo
	 * actually in use (dot-map-small.png) is dark type on a WHITE plate, and
	 * brightness(0) invert(1) turned the entire plate white -- a solid white box
	 * on the navy footer, on every page. Measured live 2026-09-12.
	 *
	 * Unfiltered it at least stays legible. The real fix is a transparent-
	 * background or reversed-out logo asset; until then, do not reintroduce the
	 * filter on an image whose alpha channel nobody has checked.
	 */
}

@media (min-width: 900px) {
	.dm-page .dm-logo--bundled.dm-logo--header {
		width: 88px;
		height: 33px;
	}

	.dm-page .dm-logo--custom.dm-logo--header {
		height: 40px;
	}
}


/* --- Mobile nav toggle --------------------------------------------------- */

.dm-page .dm-nav-toggle {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	/* 44px: the WCAG 2.2 AA target-size minimum, met by the control itself
	   rather than by spacing around it. */
	width: 44px;
	height: 44px;
	/* Optical alignment: pull the icon back to the container edge so it lines
	   up with the text below it rather than sitting 10px inboard. */
	margin-inline-end: calc(var(--space-2) * -1);
	border: var(--border-width-hairline) solid transparent;
	border-radius: var(--radius-md);
	background-color: transparent;
	color: var(--text-primary);
	transition:
		background-color var(--duration-fast) var(--ease-standard),
		border-color var(--duration-fast) var(--ease-standard),
		color var(--duration-fast) var(--ease-standard);
}

.dm-page .dm-nav-toggle:hover {
	background-color: var(--bg-sunken);
	border-color: var(--border-default);
}

.dm-page .dm-nav-toggle[aria-expanded="true"] {
	background-color: var(--bg-sunken);
	border-color: var(--border-default);
	color: var(--brand-primary);
}

.dm-page .dm-nav-toggle__icon {
	display: block;
	width: 22px;
	height: 22px;
	/* Lucide-style: 1.75px stroke, no fill, rounded joins. */
	stroke: currentColor;
	fill: none;
}

/* Exactly one of the two icons is in the DOM flow at a time. A swap, not a
   morph — no rotation, no cross-fade. */
.dm-page .dm-nav-toggle .dm-nav-toggle__icon--close,
.dm-page .dm-nav-toggle[aria-expanded="true"] .dm-nav-toggle__icon--open {
	display: none;
}

.dm-page .dm-nav-toggle[aria-expanded="true"] .dm-nav-toggle__icon--close {
	display: block;
}

@media (min-width: 900px) {
	/* The horizontal nav takes over; the control has nothing left to do. */
	.dm-page .dm-nav-toggle {
		display: none;
	}
}


/* --- Primary nav --------------------------------------------------------- */

.dm-page .dm-nav {
	/* Collapsed by default. This is the phone state AND the no-JS state. */
	display: none;
	/* Full-width row beneath the logo/toggle row once it wraps. */
	flex-basis: 100%;
	order: 3;
	padding-block-start: var(--space-2);
	border-top: var(--border-width-hairline) solid var(--border-default);
}

.dm-page .dm-nav.is-open {
	display: block;
}

.dm-page .dm-nav__list {
	display: flex;
	flex-direction: column;
	gap: 0;
	margin: 0;
	padding: 0;
	list-style: none;
}

.dm-page .dm-nav__item + .dm-nav__item {
	border-top: var(--border-width-hairline) solid var(--ink-50);
}

.dm-page .dm-nav__link {
	display: block;
	/* Comfortable 44px+ touch target across the full panel width. */
	padding: var(--space-3) var(--space-2);
	min-height: 44px;
	border-radius: var(--radius-md);
	color: var(--text-secondary);
	font-family: var(--font-sans);
	font-size: var(--text-body-sm);
	font-weight: var(--weight-semibold);
	line-height: 1.5rem;
	text-decoration: none;
	transition:
		color var(--duration-fast) var(--ease-standard),
		background-color var(--duration-fast) var(--ease-standard);
}

.dm-page .dm-nav__link:hover {
	background-color: var(--bg-sunken);
	color: var(--brand-primary);
	text-decoration: none;
}

/* The current page. Both the WordPress menu class and the fallback's own
   class are matched, plus aria-current so the visual and the accessible
   states can never drift apart. */
.dm-page .dm-nav__item.is-current > .dm-nav__link,
.dm-page .dm-nav__item.current-menu-item > .dm-nav__link,
.dm-page .dm-nav__link[aria-current="page"] {
	color: var(--brand-primary);
}

@media (min-width: 900px) {
	.dm-page .dm-nav {
		display: block;
		flex-basis: auto;
		order: 0;
		padding-block-start: 0;
		border-top: 0;
	}

	/*
	 * There is deliberately NO `.dm-nav[hidden] { display: block }` rule here.
	 *
	 * base.css enforces `[hidden] { display: none !important }`, which such a
	 * rule could not beat — it would sit in the file looking like a safety net
	 * while doing nothing, which is worse than no safety net at all.
	 *
	 * `hidden` is owned solely by nav.js, which sets it only below 900px and
	 * clears it on the way up (see syncToBreakpoint). With JavaScript off, the
	 * attribute is never set in the first place, so the desktop nav shows
	 * normally from the markup alone.
	 */

	.dm-page .dm-nav__list {
		flex-direction: row;
		align-items: center;
		gap: var(--space-8); /* 32px; the kit's 28 is not on the token scale */
	}

	.dm-page .dm-nav__item + .dm-nav__item {
		border-top: 0;
	}

	.dm-page .dm-nav__link {
		min-height: 0;
		padding: var(--space-1) 0;
		border-radius: 0;
	}

	.dm-page .dm-nav__link:hover {
		/* A background swatch behind a single inline word reads as a button.
		   On the desktop row the hover is colour only. */
		background-color: transparent;
	}
}


/* ==========================================================================
   7. Site footer
   --------------------------------------------------------------------------
   Footer.jsx spec: --navy-950 background, --ink-300 text, mono at 13px,
   28px/48px padding, flex row space-between of [sign-off | nav | logo], and
   the wordmark knocked out to white.

   --ink-300 (#a2a9ae) on --navy-950 (#0a141e) measures 7.8:1 — comfortably
   past the 4.5:1 AA threshold, so the kit's colour pairing ships unchanged.

   Mono at 13px is --text-mono-label exactly.

   NET-NEW: the stacked phone layout, the focus treatment (handled in
   base.css via the .dm-site-footer :focus-visible rule), and the fact that
   this nav doubles as the no-JavaScript sitemap.
   ========================================================================== */

.dm-page .dm-site-footer {
	background-color: var(--navy-950);
	color: var(--ink-300);
	font-family: var(--font-mono);
	font-size: var(--text-mono-label);
	line-height: var(--leading-normal);
}

.dm-page .dm-site-footer__inner {
	display: flex;
	flex-direction: column;
	align-items: flex-start;
	gap: var(--space-6);
	padding-block: var(--space-8); /* 32px */
}

.dm-page .dm-site-footer__signoff {
	margin: 0;
	max-width: 46ch;
	color: var(--ink-300);
}

.dm-page .dm-footer-nav__list {
	display: flex;
	flex-wrap: wrap;
	gap: var(--space-2) var(--space-5); /* 8px row, 20px column */
	margin: 0;
	padding: 0;
	list-style: none;
}

.dm-page .dm-footer-nav__link {
	display: inline-block;
	/* Vertical padding rather than a taller line-height keeps the 13px mono
	   rhythm while still giving a usable touch target on a phone. */
	padding-block: var(--space-1);
	border-radius: var(--radius-sm);
	color: var(--ink-300);
	text-decoration: none;
	transition: color var(--duration-fast) var(--ease-standard);
}

.dm-page .dm-footer-nav__link:hover {
	color: var(--ink-0);
	text-decoration: underline;
	text-underline-offset: 0.2em;
}

.dm-page .dm-footer-nav__item.is-current > .dm-footer-nav__link,
.dm-page .dm-footer-nav__item.current-menu-item > .dm-footer-nav__link,
.dm-page .dm-footer-nav__link[aria-current="page"] {
	color: var(--ink-0);
}

.dm-page .dm-site-footer__brand {
	display: flex;
	align-items: center;
}

@media (min-width: 900px) {
	.dm-page .dm-site-footer__inner {
		flex-direction: row;
		align-items: center;
		justify-content: space-between;
		gap: var(--space-8);
	}

	.dm-page .dm-site-footer__signoff {
		max-width: none;
	}
}


/* ===========================================================================
 * DARK CHROME
 *
 * The header and every page hero share one continuous navy-950 field, so the
 * top of the page reads as a single dark band rather than a white bar sitting
 * on a dark hero. Nav sits in white; the current page and hover states use the
 * Geo Teal accent.
 *
 * Appended deliberately rather than edited in place: these rules override the
 * light-chrome block above at the same specificity, later in the cascade, so
 * reverting is a matter of deleting this one block.
 * ======================================================================== */

.dm-page .dm-site-header {
	/* No bottom hairline -- it would draw a line across the continuous band. */
	border-bottom: 0;
	background-color: var(--navy-950);
}

/* Nav links -------------------------------------------------------------- */

.dm-page .dm-nav__link {
	color: var(--ink-50);
}

.dm-page .dm-nav__link:hover,
.dm-page .dm-nav__link:focus-visible {
	color: var(--teal-300);
	background-color: transparent;
}

.dm-page .dm-nav__link[aria-current="page"] {
	color: var(--teal-300);
}

/* Mobile disclosure panel ------------------------------------------------ */

.dm-page .dm-nav {
	background-color: var(--navy-950);
}

.dm-page .dm-nav__link:hover {
	background-color: var(--navy-800);
}

/* Toggle ----------------------------------------------------------------- */

.dm-page .dm-nav-toggle {
	color: var(--ink-50);
	border-color: transparent;
}

.dm-page .dm-nav-toggle:hover {
	color: var(--teal-300);
	background-color: var(--navy-800);
}

.dm-page .dm-nav-toggle[aria-expanded="true"] {
	color: var(--teal-300);
}

/* Wordmark --------------------------------------------------------------- */

/*
 * NO FILTER. The bundled wordmark is now the operator's white PNG -- pure
 * white ink on real alpha -- so it is already the right colour on the navy
 * field. `brightness(0) invert(1)` on white artwork is a no-op that survives
 * only to confuse whoever reads this next.
 */

/*
 * A Customizer upload gets NO filter. The logo currently uploaded
 * (dot-map-small.png) is dark type on a solid WHITE plate; inverting it
 * produced a solid white box in the footer, and would do the same here.
 * Unfiltered it renders as a white plate on navy: legible, but not the intent.
 *
 * TO GET A CLEAN WHITE WORDMARK IN THE DARK HEADER, either remove the
 * Customizer logo (Appearance -> Customize -> Site Identity) so the bundled
 * transparent wordmark is used, or upload a transparent-background /
 * reversed-out version.
 */

/* Focus rings on the dark band ------------------------------------------- */

/*
 * base.css switches the ring to the light end of the ink scale inside
 * .dm-on-dark and the footer. The header is now dark too, so it needs the same
 * treatment or the navy-500 ring disappears into the navy-950 ground.
 */
.dm-page .dm-site-header :focus-visible {
	outline-color: var(--ink-0);
	box-shadow: none;
}


/* --- Dark chrome, cascade correction ---------------------------------------
 *
 * The current-page nav link measured rgb(30,58,86) on navy-950 = 1.59:1,
 * effectively invisible, on every page and in the open mobile drawer.
 *
 * Cause: the light-chrome rule earlier in this file is
 *   .dm-page .dm-nav__item.is-current > .dm-nav__link   -> (0,4,0)
 * while the dark-chrome rule above is
 *   .dm-page .dm-nav__link[aria-current="page"]         -> (0,3,0)
 * The markup emits BOTH hooks, and specificity beats source order, so navy won.
 *
 * Matching both hooks at (0,4,0) settles it. A shipped declaration is not an
 * applied one -- the teal was in the file the whole time and never painted.
 * ------------------------------------------------------------------------- */

.dm-page .dm-nav__item.is-current > .dm-nav__link,
.dm-page .dm-nav__item > .dm-nav__link[aria-current="page"] {
	color: var(--teal-300);
}

.dm-page .dm-nav__item.is-current > .dm-nav__link:hover,
.dm-page .dm-nav__item > .dm-nav__link[aria-current="page"]:hover {
	color: var(--teal-300);
	background-color: var(--navy-800);
}


/* --- Wordmark optical alignment ------------------------------------------
 *
 * Measured from rendered pixels: the logo's INK starts at x=157 while every
 * text column on the page starts at x=144-146. The mark is on the grid; its
 * artwork is not -- the PNG carries ~20% transparent padding on each side.
 *
 * `object-fit: cover` is already applied above to crop that padding, but at
 * this box ratio cover crops the VERTICAL axis only, so the whole horizontal
 * indent survives. Hence an explicit optical pull.
 *
 * CALIBRATED TO THE CURRENT ASSET. Delete this rule the moment a trimmed
 * wordmark (ideally an SVG) replaces dot-map-logo.png -- against trimmed
 * artwork it would pull the mark 12px off the grid in the other direction.
 * ---------------------------------------------------------------------- */

.dm-page .dm-logo--bundled.dm-logo--header {
	/*
	 * Optical pull, recalibrated for the new asset. Its ink starts 38px into
	 * an 800px canvas; at a 33px rendered height that is ~4.2px of transparent
	 * margin, against the previous asset's ~12px. Delete this line entirely if
	 * a fully trimmed wordmark or an SVG ever replaces the PNG.
	 */
	margin-inline-start: -4px;
}


/* --- Nav and footer hit areas ------------------------------------------- */

/*
 * Measured: header nav link 55x32, footer nav link 31x28 -- the smallest
 * targets on the site, and the footer ones sit close together. The mobile nav
 * toggle is already a correct 44x44, so the standard was understood; the links
 * just never got it.
 */
@media (min-width: 900px) {
	.dm-page .dm-nav__link {
		display: inline-flex;
		align-items: center;
		min-height: var(--space-10); /* 40px */
		padding-block: var(--space-2);
	}
}

.dm-page .dm-footer-nav__link {
	display: inline-flex;
	align-items: center;
	min-height: var(--space-10);
	padding-block: var(--space-2);
	padding-inline: var(--space-2);
}

/* The enlarged targets would otherwise touch. */
.dm-page .dm-footer-nav__list {
	gap: var(--space-3) var(--space-4);
}

/*
 * .dm-footer-nav__link already carried a border-radius with no background in
 * any state -- a radius on a transparent inline box is inert. A hover ground
 * makes both the radius and the enlarged hit area visible.
 */
.dm-page .dm-footer-nav__link:hover,
.dm-page .dm-footer-nav__link:focus-visible {
	background-color: var(--navy-800);
	color: var(--ink-0);
}


/* ===========================================================================
 * HERO — DRIFTING GRADIENT + TYPEWRITER            (rebuilt 2026-09-13)
 *
 * NOTE ON THE BRAND RULE. The design system says "Flat colour fields only. No
 * gradients." This block is a deliberate, operator-approved exception, limited
 * to the hero band and the CTA band.
 *
 * ---------------------------------------------------------------------------
 * WHY THIS WAS REBUILT — the three measured causes, so they are not repeated
 * ---------------------------------------------------------------------------
 *
 * 1. AMPLITUDE, not technique. The first sets used four near-black stops
 *    spanning L 0.0066 -> 0.0267: a luminance spread of 0.0201. That produces
 *    2-5/255 of change per second, with std(delta) at the PNG noise floor.
 *    codepen.io/P1N2O/pen/pyBNzX is visible because #ee7752 / #e73c7e /
 *    #23a6d5 / #23d5ab span L 0.14 -> 0.55, not because of anything in its
 *    three lines of CSS. Copying the technique without the range cannot work.
 *
 * 2. THE SCRIM. The fix for (1) was saturated stops plus a 0.95-alpha navy
 *    film over the text column. A translucent film desaturates whatever is
 *    under it, so the band went muddy and the only surviving colour was a
 *    strip at the edges where the film had thinned. Measured live before this
 *    rebuild: white on the teal-500 stop was 4.47:1 and on teal-400 2.91:1 —
 *    the gradient could not be allowed out from under the film without
 *    failing AA, so the film could never be thinned, so the colour could
 *    never be seen. A closed loop.
 *
 * 3. background-size: 400% 400%. On a 1425x576 band that puts adjacent stops
 *    roughly 1900px apart along the axis, so every transition is a soft ramp
 *    wider than the viewport and there is no edge for motion detection to
 *    lock onto. The pen gets away with it on a `height: 100vh` body. This
 *    band is 576px tall and does not.
 *
 * ---------------------------------------------------------------------------
 * THE SHAPE OF THE FIX — mask the colour, do not veil it
 * ---------------------------------------------------------------------------
 *
 * Two layers with two independent palettes, and nothing painted over text:
 *
 *   LAYER 1 (here)      whole band, including under the words. Every stop is
 *                       contrast-legal on its own, so no film is needed at
 *                       all. Spread L 0.0066 -> 0.0796 = 0.0730, 3.6x the
 *                       0.0201 that measured as invisible.
 *   LAYER 2 (::after)   masked to nothing before it reaches the text column.
 *                       Nothing on it has to be read, so it runs at full
 *                       saturation: spread 0.3620, 18x.
 *
 * MEASURED ON THE REBUILT BAND, eight phases across the 19s cycle, sampling
 * the brightest background pixel inside each text element's box:
 *     h1       white     worst 7.34:1
 *     eyebrow  teal-200  worst 5.31:1
 *     lede     ink-100   worst 6.23:1
 * Do not add a lighter stop to layer 1 without re-running that sweep. An
 * animated ground can pass at one frame and fail at another, which is exactly
 * how the previous set shipped failing.
 *
 * THE EYEBROW MOVED teal-300 -> teal-200 and that is load-bearing. teal-300
 * against layer 1's lightest stop measures 3.99:1. The alternative was to cap
 * layer 1 at teal-800, which drops the spread to 0.0415 — 2.1x — and heads
 * straight back into the range that has already failed twice.
 *
 * The hero is the FIRST top-level Elementor container on every page.
 * Elementor 4.2.4 drops `_css_classes` on containers, so there is no bespoke
 * hook and structural position is the only reliable selector.
 * ======================================================================== */

/* The reference keyframes, verbatim. */
@keyframes gradient {
	0%   { background-position: 0% 50%; }
	50%  { background-position: 100% 50%; }
	100% { background-position: 0% 50%; }
}

body.dm-page .elementor:not(.elementor-location-header):not(.elementor-location-footer) > .e-con:first-child {
	/* LAYER 1 — the contrast-safe field. */
	background-image: linear-gradient(
		-42deg,
		var(--navy-950)   0%,
		var(--teal-800)  15%,
		var(--navy-700)  30%,
		var(--teal-700)  44%,
		var(--navy-950)  57%,
		var(--navy-600)  72%,
		var(--teal-800)  87%,
		var(--navy-950) 100%
	);
	/*
	 * 210%, NOT the reference's 400%, and eight stops rather than four.
	 * Together those put a stop transition roughly every 370px along the
	 * axis instead of every ~1900px, so three or four of them are inside the
	 * band at once and the thing that moves is an EDGE. A single soft ramp
	 * wider than the viewport is a colour change; a travelling edge is
	 * motion, and the eye only reliably registers the second one.
	 */
	background-size: 210% 210%;
	/*
	 * THE STATIC FRAME, and the reason `background-image: none` is gone from
	 * the reduced-motion block below.
	 *
	 * Whatever is painted when nothing is animating -- no JavaScript,
	 * prefers-reduced-motion, or the pause button pressed -- is this. The
	 * previous build DELETED the background image under reduced motion, so a
	 * visitor with that setting on saw a flat navy void and zero trace of the
	 * effect no matter what shipped. Reduced motion means do not move it; it
	 * has never meant do not paint it. 64% lands mid-cycle, with colour on
	 * the band rather than at the dark end of the ramp.
	 */
	background-position: 64% 50%;
}

/* Gated: nothing loops until hero.js has put the pause control on the page. */
html.dm-motion-ready body.dm-page .elementor:not(.elementor-location-header):not(.elementor-location-footer) > .e-con:first-child {
	/*
	 * `linear`, not the reference's `ease`. On a 19s loop `ease` parks the
	 * field for most of the cycle: measured, half the cycle changed by under
	 * 1/255 per second, so the motion arrived in two bursts and was frozen in
	 * between. Constant travel beats eased travel when the loop is this long.
	 */
	animation: gradient 19s linear infinite;
}

/*
 * The parcel grid drifts the other way at a fraction of the speed. Two layers
 * moving at different rates in opposite directions is what reads as depth
 * rather than as moving wallpaper, and it is still only background-position
 * changing -- nothing transforms, nothing about the layout moves.
 */
@keyframes dm-motif-drift {
	from { background-position: right -40px top -40px; }
	to   { background-position: right -360px top -40px; }
}

html.dm-motion-ready body.dm-page .elementor:not(.elementor-location-header):not(.elementor-location-footer) > .e-con:first-child::before {
	animation: dm-motif-drift 150s linear infinite;
}

/* ---------------------------------------------------------------------------
 * PAUSE GOVERNS EVERY LOOPING ANIMATION ON THE PAGE
 *
 * ONE RULE, NOT A LIST. This was four hand-written selectors naming the
 * animations that existed when it was written; there are now eight, and the
 * list had already fallen behind twice. A pause control that covers four of
 * eight is the same failure as one that covers none, only harder to spot.
 *
 * THE !important IS DELIBERATE. Every gated rule sets `animation` via the
 * SHORTHAND, which resets animation-play-state to `running` as part of its
 * own declaration. Beating eight shorthand selectors on specificity means
 * eight more selectors to keep in sync forever, which is precisely the
 * maintenance failure that produced this bug. WCAG 2.2.2 is not a place for a
 * rule that works until someone adds the ninth animation.
 *
 * Scoped to .dm-page so it cannot reach the WordPress admin bar or any plugin
 * UI outside this theme's markup.
 *
 * VERIFY BY COMPUTED VALUE, NOT BY READING. The test is
 * `getComputedStyle(el).animationPlayState === "paused"` plus a background
 * position that stops advancing. The presence of the declaration proves
 * nothing — that is exactly how this shipped broken.
 * ------------------------------------------------------------------------ */
html.dm-motion-paused body.dm-page *,
html.dm-motion-paused body.dm-page *::before,
html.dm-motion-paused body.dm-page *::after {
	animation-play-state: paused !important;
}

/* --- Typewriter ------------------------------------------------------------
 *
 * The reference effect:
 *
 *     animation: typing 3.5s steps(40, end),
 *                blink-caret .75s step-end infinite;
 *
 * with `overflow: hidden`, `white-space: nowrap` and width animating 0 -> 100%
 * behind a `border-right` caret ON THE TEXT ELEMENT. All of that is kept. Two
 * things changed, and both were visible defects rather than preferences.
 *
 * 1. `to { width: var(--dm-w) }`, NOT `to { width: 100% }`.
 *
 *    The reference is a single line in a box that is roughly text-width, so
 *    100% and "the width of the text" are nearly the same number and the
 *    caret lands on the last glyph. Here each line is a block inside a 56%
 *    column: 100% is the COLUMN width. Measured on the live site, the caret
 *    rode the column edge up to 400px clear of the last character it was
 *    supposed to be sitting behind, and the final line's caret — which starts
 *    blinking at t=2965ms on a line that has not begun typing — blinked in
 *    empty space three lines below the visible text for three seconds.
 *    It read as a stray teal bar, because that is what it was.
 *
 *    hero.js measures each rendered line's natural width and sets it as
 *    --dm-w on that line, so the box stops growing exactly at the last glyph.
 *
 * 2. The caret is lit on exactly one line at a time.
 *
 *    `dm-caret-on` runs for precisely the line's own typing duration with
 *    fill-mode `none`, so before its delay and after it ends the border falls
 *    back to the static `transparent`. A solid cursor while characters are
 *    being emitted and a blink when idle is also what a real terminal does;
 *    the reference blinks throughout only because it has one line and never
 *    goes idle until the end. `blink-caret` — the reference's own keyframes,
 *    unchanged — takes over on the last line once the sentence is complete.
 *
 * LETTER-SPACING. The reference's .15em was rendered and measured at this
 * heading's 46px/700: it pushes the h1 from four rendered lines to five and
 * reads as a tracked-out subtitle rather than a headline, because .15em of a
 * 46px display face is 6.9px of gap. The reference assumes body-ish sizing.
 * The h1 gets 0.04em — 1.8px, visibly airier than the -0.5px negative
 * tracking the generator sets, holding four lines, and enough to give the
 * reveal the even rhythm the tracking is there for. The full .15em goes where
 * the reference's assumptions actually hold: the mono eyebrow, 13px, one
 * line, below.
 * ------------------------------------------------------------------------ */

@keyframes typing {
	from { width: 0; }
	to   { width: var(--dm-w, 100%); }
}

@keyframes blink-caret {
	from, to { border-color: transparent; }
	50%      { border-color: var(--teal-300); }
}

/* Solid for exactly as long as this line is being typed. */
@keyframes dm-caret-on {
	from, to { border-color: var(--teal-300); }
}

body.dm-page .dm-type {
	display: block;
}

body.dm-page .dm-type__line {
	display: block;
	width: 0;
	overflow: hidden;
	white-space: nowrap;
	/* The reference's caret: a border on the text element, not a extra span. */
	border-right: 0.09em solid transparent;
}

body.dm-page .elementor:not(.elementor-location-header):not(.elementor-location-footer) > .e-con:first-child h1.elementor-heading-title {
	letter-spacing: 0.04em;
}

/*
 * The eyebrow: teal-200 rather than teal-300 (see the contrast note at the
 * top of this block) and the reference's own .15em, which at 13px monospace
 * on one line is the exact condition the reference was written for.
 *
 * Targeted structurally because Elementor 4.2.4 drops `_css_classes` on
 * containers and emits its own per-widget colour rule at (0,4,0); this lands
 * at (0,5,2) and wins outright. The path is
 *     hero > .e-con-inner > row > text column > first widget.
 */
body.dm-page .elementor:not(.elementor-location-header):not(.elementor-location-footer) > .e-con:first-child
	.e-con > .e-con:first-child
	> .elementor-widget-heading:first-child .elementor-heading-title {
	color: var(--teal-200);
	letter-spacing: 0.15em;
}

/* --- Paused: show the whole heading, do not freeze it half-typed ----------
 *
 * `animation-play-state: paused` on a width animation leaves the line at
 * whatever fraction it had reached, and `.dm-type__line` has a static width
 * of 0 — so pausing during the first 3.5s would have BLANKED the h1. A
 * control that hides the page's primary heading is not a WCAG 2.2.2
 * mechanism, it is a new failure. Pausing completes the reveal instead.
 * ---------------------------------------------------------------------- */
html.dm-motion-paused body.dm-page .dm-type__line {
	width: auto !important;
	animation: none !important;
	border-right-color: transparent !important;
}

/* --- Reduced motion: DEGRADE, do not delete ------------------------------
 *
 * What used to be here was `animation: none; background-image: none;` — it
 * removed the gradient entirely. A visitor with prefers-reduced-motion set
 * saw a flat navy void and no trace of the design, and hero.js returned
 * before doing anything, so the heading and the pause control were absent
 * too. If the operator has that setting on, that is the whole of what four
 * rebuilds of this effect delivered to them.
 *
 * Now: the gradient is still painted, at the static frame set above. It just
 * does not move. base.css already neutralises animation globally under this
 * query (duration 0.01ms, iteration 1), so nothing here needs to re-state it.
 *
 * The typewriter is belt-and-braces: hero.js does not split the heading at
 * all under this query, so `.dm-type__line` should never exist — but if the
 * setting is toggled ON after the script has run, this keeps the text
 * visible rather than collapsing it to width 0.
 * ---------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {html:not(.dm-motion-opt-in) body.dm-page .dm-type__line {
		width: auto;
		border-right-color: transparent;
		animation: none;
	}
}

@media (forced-colors: active) {
	/* Forced colours supplies its own ground; a decorative field only fights it. */
	body.dm-page .elementor:not(.elementor-location-header):not(.elementor-location-footer) > .e-con:first-child {
		background-image: none;
	}
}


/* --- Desktop nav hover: give the ground somewhere to sit -------------------
 *
 * The operator reported the hover background looked cramped. It was: the
 * desktop rule earlier in this file sets
 *
 *     .dm-page .dm-nav__link { padding: var(--space-1) 0; border-radius: 0; }
 *
 * -- 4px vertical, ZERO horizontal, square corners -- and it even set the
 * hover background to `transparent`, because at the time the desktop row had
 * no hover ground at all. The dark-chrome block then reintroduced a navy
 * background, which painted onto a box with no horizontal padding and no
 * radius. A colour with nowhere to go reads as a highlighter stripe.
 *
 * Appended last so it wins on source order at equal (0,2,0) specificity.
 * ------------------------------------------------------------------------ */

@media (min-width: 900px) {
	.dm-page .dm-nav__link {
		padding: var(--space-2) var(--space-3); /* 8px 12px */
		border-radius: var(--radius-md);
		min-height: var(--space-10);
	}

	/*
	 * NO FILL ON THE DESKTOP ROW.
	 *
	 * Measured: text 55.10px wide, hover box 55.10px wide -- 0.00px of coloured
	 * ground either side. On "Work" the box came out 34.2 wide x 40 tall,
	 * taller than it was wide. It read as a text-selection artefact rather than
	 * a pill, which is what the operator was reacting to.
	 *
	 * Padding is fixed above, but the fill is not what was carrying this state:
	 * the teal label plus the drawn underline already do, which is exactly why
	 * the current-page treatment always looked right. The footer keeps its
	 * fill -- it has 8px all round and a radius, and it works there.
	 */
	.dm-page .dm-nav__link:hover,
	.dm-page .dm-nav__link:focus-visible {
		background-color: transparent;
	}

	/*
	 * The drawn underline has to clear the new padding, or it sits halfway
	 * down the coloured ground instead of under the word.
	 */
	body.dm-page .dm-nav__link {
		background-position: left calc(100% - 8px);
	}
}


/* ===========================================================================
 * LAYER 2 — THE SATURATED FIELD, MASKED OFF THE TEXT
 *
 * WHY A SECOND LAYER EXISTS
 *
 * Contrast is what caps layer 1. White, teal-200 and ink-100 all have to
 * clear 4.5:1 against whatever colour is under them, which forbids any stop
 * above L 0.10 in the text column. That is a real ceiling and no amount of
 * tuning moves it.
 *
 * But the ceiling applies to "behind text", not to "the hero". The heading,
 * eyebrow, lede and buttons occupy the left 56% of the band; the right 44%
 * carries the parcel panel and nothing that has to be read. So the saturated
 * colour lives THERE, and is masked to nothing before it reaches the words.
 *
 * MASKED, NOT VEILED — this is the whole difference from the version the
 * operator rejected. That one painted a 0.95-alpha navy film OVER a saturated
 * gradient across the text column and thinned it to 0.12 at the right edge. A
 * translucent film desaturates everything under it, so the band read as mud
 * with a strip of colour at the margins. A mask removes the layer instead of
 * covering it: the left side is clean navy, the right side is full-strength
 * teal, and there is no third muddy colour anywhere because nothing is
 * composited over anything.
 *
 * PALETTE. No contrast constraint applies here, so these are the brightest
 * in-brand stops available:
 *     navy-500  L 0.1050      teal-500  L 0.1851
 *     teal-600  L 0.1213      navy-400  L 0.1892
 *     teal-400  L 0.3113      teal-300  L 0.4670
 * Luminance spread 0.3620 — eighteen times the 0.0201 that measured at the
 * noise floor. Signal Amber is deliberately NOT in this set: at field size it
 * pulls the band toward a sunset and the token's stated role is "CTA /
 * marker-pin accent", which is the Begin-a-project button, not the sky.
 *
 * THE MASK IS LOAD-BEARING FOR CONTRAST, NOT FOR STYLING.
 *
 * It is fully transparent until 56% and only reaches 0.35 at 66%. Measured at
 * 1440, the widest of the three text boxes (the lede's) ends at x=782 on a
 * 1425px band — 54.9% — so no glyph sits on any of this layer at all, at any
 * phase. That number only gets smaller as the viewport widens, because the
 * row is capped at 1376px and centres while the band keeps growing.
 *
 * The first cut of this mask started at 52%, which let roughly 0.10 alpha of
 * layer 2 reach the last 40px of the lede's box and dropped its worst-case
 * contrast from the analytic floor of 4.80:1 to a measured 4.78:1. Both pass;
 * the point is that four percent of mask is the difference between "passes"
 * and "cannot fail", and on an animated ground the second is the only one
 * worth having.
 *
 * Widening it leftward re-opens the problem it exists to solve — re-run the
 * eight-phase sweep if you touch it.
 * ======================================================================== */

body.dm-page .elementor:not(.elementor-location-header):not(.elementor-location-footer) > .e-con:first-child::after {
	content: "";
	position: absolute;
	inset: 0;
	/*
	 * -2, NOT -1, AND THIS PAIRS WITH .dm-float's -1 IN motif.css.
	 *
	 * A ::after is by definition the element's last child. Among siblings
	 * sharing a z-index, paint order is tree order -- so while this layer and
	 * .dm-float were both at -1, this one painted ON TOP of the marks. It is
	 * opaque wherever its own mask is opaque, which is from 76% of the band
	 * rightward, so the marks were not dimmed out there, they were covered.
	 * Measured on the live band, marks were visible only between x=798 and
	 * x=1084 of 1425px and faded across the whole of that window.
	 *
	 * Both numbers have to move together. Raising this back to -1 puts the
	 * marks back underneath the gradient with no other symptom.
	 */
	z-index: -2;
	pointer-events: none;
	background-image: linear-gradient(
		-42deg,
		var(--teal-600)   0%,
		var(--teal-400)  14%,
		var(--teal-300)  26%,
		var(--navy-500)  40%,
		var(--teal-500)  54%,
		var(--navy-400)  68%,
		var(--teal-400)  82%,
		var(--teal-600) 100%
	);
	background-size: 210% 210%;
	/* Same static frame as layer 1, so the two agree when nothing moves. */
	background-position: 64% 50%;
	-webkit-mask-image: linear-gradient(
		90deg,
		transparent 0%,
		transparent 56%,
		rgb(0 0 0 / 0.35) 66%,
		#000 76%,
		#000 100%
	);
	mask-image: linear-gradient(
		90deg,
		transparent 0%,
		transparent 56%,
		rgb(0 0 0 / 0.35) 66%,
		#000 76%,
		#000 100%
	);
}

/*
 * Both layers run the SAME keyframes at the SAME duration and offset. They
 * are two slices of one field, not two effects: if they drift apart the band
 * develops a seam at the mask boundary where the two are travelling at
 * different rates.
 */
html.dm-motion-ready body.dm-page .elementor:not(.elementor-location-header):not(.elementor-location-footer) > .e-con:first-child::after {
	animation: gradient 19s linear infinite;
}

@media (max-width: 899px) {
	/*
	 * The columns stack below 900px, so the text is no longer clear of the
	 * right-hand zone and a horizontal mask cannot protect it. Rotate to
	 * vertical rather than dropping the layer: the words sit at the TOP of a
	 * stacked hero and the panel below them, so a top-to-bottom mask
	 * reproduces exactly the same guarantee one axis over.
	 *
	 * 64%, not 58%. Measured at 390x920 the lede's box ends at y=542 — 58.9%
	 * of the band — so a mask opening at 58% put roughly a fifth of layer 2's
	 * ramp under its last line and took its worst-case contrast to 3.85:1.
	 * The desktop number does not transfer: the text is full-width here and
	 * the band is two and a half times taller relative to it.
	 */
	body.dm-page .elementor:not(.elementor-location-header):not(.elementor-location-footer) > .e-con:first-child::after {
		-webkit-mask-image: linear-gradient(
			180deg,
			transparent 0%,
			transparent 64%,
			rgb(0 0 0 / 0.35) 72%,
			#000 84%,
			#000 100%
		);
		mask-image: linear-gradient(
			180deg,
			transparent 0%,
			transparent 64%,
			rgb(0 0 0 / 0.35) 72%,
			#000 84%,
			#000 100%
		);
	}
}

@media (forced-colors: active) {
	/*
	 * Forced colours replaces the author palette wholesale. A decorative
	 * colour field carries no information and would only fight the system
	 * ground, so it goes. Layer 1 goes with it, in layout's hero block.
	 */
	body.dm-page .elementor:not(.elementor-location-header):not(.elementor-location-footer) > .e-con:first-child::after {
		display: none;
	}
}


/* ==========================================================================
   12. Elementor Theme Builder chrome
   --------------------------------------------------------------------------
   Styling for the bundled Header and Footer templates in
   elementor-layouts/dotmap-header.json and dotmap-footer.json, which render
   INSTEAD OF header.php / footer.php whenever a matching Theme Builder
   template exists (see inc/elementor.php, dotmap_elementor_did_location()).

   WHY THERE IS SO LITTLE HERE.

   Elementor 4.2.4 silently drops `_css_classes` on CONTAINERS and honours it
   on WIDGETS -- the same asymmetry that cost `.dm-formidable` its entire
   stylesheet earlier in this build. So the templates cannot rely on
   `.dm-site-header` / `.dm-site-footer` reaching the band: every colour, font,
   size and spacing in them is written as an inline Elementor setting instead,
   which is why those JSON files carry explicit hex values rather than reading
   the tokens.

   What IS reachable is a class on a widget. This block uses exactly that, for
   the one thing an Elementor setting cannot express: the wordmark's optical
   correction.
   ========================================================================== */

/*
 * The bundled PNG carries roughly 20% transparent padding on each side, so an
 * untreated <img> renders the mark smaller than its box and indented from the
 * text grid. header.php's markup gets `object-fit` and a -4px pull from the
 * rules in section 5; the Elementor Image widget emits a plain <img> and gets
 * neither, so without this the two renderers disagree by about 12px of
 * horizontal indent -- visible as the logo jumping when a Theme Builder header
 * is activated.
 *
 * Delete this whole block the moment a trimmed wordmark or an SVG replaces
 * dot-map-logo-white.png. Against trimmed artwork it pulls the wrong way.
 */
.dm-page .dm-tb-logo img,
body .dm-tb-logo img {
	object-fit: contain;
	object-position: left center;
	margin-inline-start: -4px;
}

/*
 * Elementor's Nav Menu widget renders its own markup, so the theme's
 * `.dm-nav__link` hit-area rules in section 11 never reach it. 2.5.8 Target
 * Size (AA) applies just the same.
 */
.dm-tb-nav .elementor-nav-menu > li > a,
.dm-tb-footer-nav .elementor-nav-menu > li > a {
	display: inline-flex;
	align-items: center;
	min-height: var(--space-10); /* 40px */
}

/*
 * base.css scopes the light-on-dark focus ring to `.dm-on-dark` and the
 * theme's own footer, neither of which is present on Theme Builder chrome.
 * Both templates sit on navy-950, where the default navy-500 ring is
 * invisible.
 *
 * Selectors are the WIDGET classes only. The first cut of this rule led with
 * `.dm-tb-header` and `.dm-tb-footer` -- container classes, which Elementor
 * drops, exactly as the note at the top of this section says. Every focusable
 * thing in both templates is inside a widget, so the widget hooks reach all
 * of them anyway.
 */
.dm-tb-nav :focus-visible,
.dm-tb-footer-nav :focus-visible,
.dm-tb-logo :focus-visible,
.dm-tb-logo a:focus-visible {
	outline: 2px solid var(--ink-0);
	outline-offset: 2px;
	box-shadow: none;
}
