/* ==========================================================================
   POPPINS, SELF-HOSTED (2026-08-13)
   --------------------------------------------------------------------------
   The single biggest reason this site did not feel like p3mai.com: p3mai.com
   is set in Poppins and this was set in whatever the operating system supplied.
   Colour and spacing can be matched token by token; a different typeface is a
   different voice.

   Self-hosted, not Google Fonts: this site's CSP is default-src 'self' with no
   font-src exception, so a Google Fonts request is blocked outright. Same four
   latin weights p3mai.com serves, same files, 30.7 KB total.

   font-display: swap -- text paints immediately in the fallback and swaps when
   Poppins arrives. Never remove it. */
@font-face { font-family:'Poppins'; font-style:normal; font-weight:400;
  font-display:swap; src:url('../assets/fonts/poppins-400-latin.woff2') format('woff2');
  unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
@font-face { font-family:'Poppins'; font-style:normal; font-weight:500;
  font-display:swap; src:url('../assets/fonts/poppins-500-latin.woff2') format('woff2');
  unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
@font-face { font-family:'Poppins'; font-style:normal; font-weight:600;
  font-display:swap; src:url('../assets/fonts/poppins-600-latin.woff2') format('woff2');
  unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
@font-face { font-family:'Poppins'; font-style:normal; font-weight:700;
  font-display:swap; src:url('../assets/fonts/poppins-700-latin.woff2') format('woff2');
  unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }

/* ---------------------------------------------------------------------------
   THE METRICS-MATCHED FALLBACK (AG-142)

   Poppins is 8.2% wider than the font that stands in for it while it loads.
   So when it arrives the text re-wraps, every block below gains or loses a
   line, and the page jumps. That is the whole of this site's CLS: 0 through
   11 August, 0.124 from the day Poppins was self-hosted.

   THE NUMBERS ARE MEASURED, NOT COPIED.
     Poppins 400, read from the woff2 with fontTools:
       unitsPerEm 1000 · ascent 1050 · descent -350 · lineGap 100
       mean advance over 56 latin glyphs: 0.5717 em
     Segoe UI, measured in the browser on this machine:
       mean advance over the same glyphs: 0.5283 em
     0.5717 / 0.5283 = 1.0822  ->  size-adjust: 108.22%

   VERIFIED BY RENDERING, NOT BY ARITHMETIC. The site's real body copy, at the
   site's real size, in a 420px column:
       Poppins                407.8px, 15 lines
       this fallback          407.8px, 15 lines   difference 0.0px
       Segoe UI unadjusted    380.6px, 14 lines   difference 27.2px  <- the jump
   And sabotage-tested: setting size-adjust back to 100% reproduces the 27.2px
   difference exactly, so the check is known to be capable of failing.

   FOUR FACES, ONE PER WEIGHT, AND THE FIRST ATTEMPT HAD ONLY ONE.
   That first version measured Poppins 400 against Segoe UI 400 and declared a
   single weight-agnostic face -- and it changed CLS by 0.0008, which is
   nothing. The element that actually moves is the h1, at weight 600, where
   Poppins is 0.5965 em against Segoe's 0.5475: a 108.95% ratio, not 108.22%.
   A single face cannot serve four ratios, and the one it did serve was the
   weight that was not shifting.

   ONLY size-adjust DOES ANY WORK ON THIS SITE, and it is worth saying so.
   The three vertical overrides below only affect a line box when line-height
   is `normal`, and nothing here computes to `normal` -- every element inherits
   1.6 or 1.2. They are declared anyway because they are correct, they cost
   nothing, and the day somebody adds a component with a default line-height
   they will already be right.

   `local()` only: this face must never fetch anything. It exists to be
   available instantly, which is the one job it has.
   --------------------------------------------------------------------------- */
@font-face {
  font-family: 'Poppins Fallback';
  font-weight: 400;
  src: local('Segoe UI'), local('Roboto'), local('Helvetica Neue'), local('Arial');
  size-adjust: 108.21%;
  ascent-override: 97.03%;
  descent-override: 32.34%;
  line-gap-override: 9.24%;
}
@font-face {
  font-family: 'Poppins Fallback';
  font-weight: 500;
  src: local('Segoe UI'), local('Roboto'), local('Helvetica Neue'), local('Arial');
  size-adjust: 106.76%;
  ascent-override: 98.35%;
  descent-override: 32.78%;
  line-gap-override: 9.37%;
}
@font-face {
  font-family: 'Poppins Fallback';
  font-weight: 600;
  src: local('Segoe UI'), local('Roboto'), local('Helvetica Neue'), local('Arial');
  size-adjust: 108.96%;
  ascent-override: 96.37%;
  descent-override: 32.12%;
  line-gap-override: 9.18%;
}
@font-face {
  font-family: 'Poppins Fallback';
  font-weight: 700;
  src: local('Segoe UI'), local('Roboto'), local('Helvetica Neue'), local('Arial');
  size-adjust: 106.80%;
  ascent-override: 98.32%;
  descent-override: 32.77%;
  line-gap-override: 9.36%;
}

/* WebSandpit — design system + styles.
   Tokens first (design-build skill §1): all page styles consume these custom
   properties — no hard-coded colours/sizes below the tokens block.

   Restraint is the brief's central instruction (client-brief.md §3). Apple's
   home page uses TWO font weights; this file uses the same two. Weight is the
   easiest thing to add and the hardest to take back, and a disciplined scale
   costs nothing — which matters on a startup site with no photography budget. */

:root {
  /* Colour: P3MAI's brand, unchanged (§4). Navy + gold + a 5-step neutral
     scale. Every text/background pairing must pass WCAG AA (4.5:1). */
  /* P3MAI's ACTUAL navy, 2026-08-13. This file claimed "P3MAI's brand,
     unchanged" while using #1a2a4a, which is not P3MAI's navy -- p3mai.com
     uses #0B2545. The two sites had drifted into different blues while the
     comment asserted they had not, which is the worst kind of drift: the one
     the file denies. */
  --color-primary: #0B2545;
  --color-primary-dk: #071830;   /* p3mai.com's dark navy */
  --color-accent: #c9a227;
  --color-accent-text: #8c6b15;  /* 4.97:1 on white — brand gold FAILS as text */
  --color-n900: #17181c;
  --color-n700: #3d4048;         /* 9.4:1 on white — safe for body copy */
  --color-n400: #9aa0ab;
  --color-n200: #d9dde3;
  --color-n100: #eef0f3;
  --color-n000: #ffffff;

  /* Type: system stack (no download = fast, and nothing to licence).
     1.25 modular scale. Only 400 and 600 are ever used — see note above. */
  /* Poppins first, matching p3mai.com's --font-main exactly. */
  /* 'Poppins Fallback' sits between the real face and the system stack: it
     IS Segoe UI, scaled so it occupies exactly the space Poppins will. */
  --font-body: 'Poppins', 'Poppins Fallback', 'Segoe UI', system-ui, -apple-system, sans-serif;
  --fs-sm: 0.8rem;
  --fs-base: 1rem;
  --fs-md: 1.25rem;
  --fs-lg: 1.563rem;
  --fs-xl: 1.953rem;
  --fs-2xl: 2.441rem;

  /* Spacing: single 8px-based scale used for all margins/padding/gaps */
  --sp-1: 0.5rem;
  --sp-2: 1rem;
  --sp-3: 1.5rem;
  --sp-4: 2.5rem;
  --sp-5: 4rem;
  --sp-6: 6rem;

  --measure: 34rem;      /* body copy line length — ~65 characters */
  /* 1140px at a 16px root — the SAME figure p3mai.com uses for .container.
     A visitor arriving here from p3mai.com sees the page edges stay put
     instead of jumping 52px, which is the kind of difference nobody names
     but everybody notices. Change this only alongside p3mai.com. */
  --container: 71.25rem;
  /* P3MAI's radius scale. This was a single flat 6px, which is what made the
     two sites feel crisp-versus-soft even once the colours matched.
     --radius stays as the name everything already uses, remapped to P3MAI's
     medium so existing rules pick up the change without being rewritten. */
  --radius-sm: 8px;
  --radius: 16px;
  --radius-lg: 28px;
  /* Navy-tinted depth, from p3mai.com. There were no shadow tokens at all,
     so every surface sat flat on the page. */
  --shadow-sm: 0 2px 8px rgba(11, 37, 69, 0.08);
  --shadow-md: 0 8px 24px rgba(11, 37, 69, 0.12);
  --shadow-lg: 0 16px 40px rgba(11, 37, 69, 0.20);
}

/* ---- Reset ----------------------------------------------------------------
   Deliberately tiny. A full reset library would be more code than this entire
   stylesheet, and most of what it undoes we never use. Two rules cover it:
   predictable box sizing, and no surprise margins. */
/* Minimal reset */
*, *::before, *::after { box-sizing: border-box; margin: 0; }

body {
  font-family: var(--font-body);
  font-size: var(--fs-base);
  font-weight: 400;
  line-height: 1.6;
  color: var(--color-n900);
  background: var(--color-n000);
  /* Safari has never supported unprefixed `hyphens`, and a static prefix audit
     caught exactly this omission once already. Keep both declarations. */
  -webkit-hyphens: manual;
  hyphens: manual;
}

/* Images never overflow their container, and keep their aspect ratio when the
   container is narrower than the file. This one line is what stops a large
   photograph forcing the whole page to scroll sideways on a phone. */
img { max-width: 100%; height: auto; }

h1, h2, h3 { font-weight: 600; line-height: 1.2; color: var(--color-primary); }
h1 { font-size: var(--fs-xl); }
h2 { font-size: var(--fs-lg); }
h3 { font-size: var(--fs-md); }
p { max-width: var(--measure); }

/* Vertical rhythm for prose. The reset zeroes all margins, so without these
   consecutive paragraphs touch and headings sit flush against the text above
   them — which showed up on the prose-heavy pages, not on the card ones. */
p + p { margin-top: var(--sp-2); }
.section .inner > * + h2 { margin-top: var(--sp-4); }
.section .inner > h2 + p { margin-top: var(--sp-2); }

a { color: var(--color-primary); }
a:hover { color: var(--color-accent-text); }

/* Skip link is visually hidden until keyboard focus lands on it */
/* Lets a keyboard user jump straight past the navigation to the content.
   Off-screen until focused, so it costs sighted users nothing and saves a
   keyboard user tabbing through six links on every single page. */
.skip-link {
  position: absolute;
  left: -999px;
  background: var(--color-primary);
  color: var(--color-n000);
  padding: var(--sp-1) var(--sp-2);
}
.skip-link:focus { left: var(--sp-1); top: var(--sp-1); z-index: 20; }

/* Visible focus everywhere — a11y checklist requirement, never remove */
:focus-visible { outline: 3px solid var(--color-accent); outline-offset: 2px; }

/* ---- Header ---------------------------------------------------------------
   Wraps rather than collapsing to a burger. At 360px the six-item nav becomes
   two rows, which costs a little height and removes an entire interactive
   component — nothing to open, nothing to trap focus in, nothing to break. */
.site-header {
  /* Aligned to p3mai.com, 2026-08-13. This was an opaque navy band; p3mai.com's
     header is LIGHT, translucent and sticky, and that single difference was
     what made the two sites read as different products more than anything
     else. The geometry already matched exactly -- 18px/24px padding, 8px/16px
     pills, 999px radius -- so only the treatment was out of step.

     Full-bleed background with .header-inner carrying the width, so the bar
     reaches both edges while its CONTENTS line up with the page below. */
  position: sticky;
  top: 0;
  z-index: 100;
  /* LIGHT, exactly as p3mai.com. Settled 2026-08-13 after seeing both: the
     navy band was tried against the new Poppins type and Douglas judged it
     poor. These are p3mai.com's own values, not an approximation of them --
     same translucency, same blur, same 1px shadow.

     The site is otherwise navy-and-gold; it is the HEADER that is light, and
     the contrast pairs below are measured for that ground. */
  background: rgba(250, 249, 246, 0.92);
  /* -webkit- first: Safari has never shipped this unprefixed, and there is no
     WebKit engine on this machine -- so a missing prefix is invisible here and
     shows up only on somebody's iPhone, as a header that is flatly opaque
     instead of frosted. */
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  box-shadow: 0 1px 0 rgba(45, 52, 54, 0.06);
}
.header-inner {
  display: flex;
  flex-wrap: wrap;
  /* 8px, the same gap the pills use. This was 16px with space-between, which
     left a wide void between the nav and the P3MAI link and made it read as a
     separate control marooned on the right rather than the last pill in the
     row. The brand takes margin-right:auto instead, so it holds the left edge
     and EVERYTHING else groups right at a single consistent spacing. */
  gap: 8px;
  align-items: center;
  justify-content: flex-start;
  max-width: var(--container);
  margin: 0 auto;
  /* 18px 24px matches p3mai.com's .navbar exactly. */
  padding: 18px 24px;
}
.brand {
  display: flex;
  align-items: center;
  margin-right: auto;   /* holds the left edge; the rest groups to the right */
  /* 10px, measured off p3mai.com's rendered logo rather than guessed. */
  gap: 10px;
  color: var(--color-primary);
  font-size: var(--fs-md);
  font-weight: 600;
  text-decoration: none;
}
/* 40px, matching p3mai.com's .logo-mark. It was 28px, which read as a
   smaller, lesser sibling of the same brand. */
.brand img { width: 40px; height: 40px; }

/* THE WORDMARK IS TWO-TONE, always: "Web" in the primary, "Sandpit" in the
   brand gold -- exactly as P3MAI's own wordmark puts "AI" in gold. It was flat
   single-colour text everywhere until 2026-08-13, which broke the house rule
   that the wordmark is never flat.

   This uses --color-accent (#c9a227), the UNDILUTED brand gold, and not
   --color-accent-text. That is deliberate and it is the same decision taken on
   p3mai.com the same day:

     WCAG 2.2 SC 1.4.3 Contrast (Minimum) excepts "text that is part of a logo
     or brand name" from the contrast requirement.

   An automated audit will flag every one of these. They are a cited exemption,
   not a defect -- and the exemption covers the LOGO, not the colour. Used for
   ordinary text this gold is a real failure and must use --color-accent-text
   (#8c6b15) instead. */
.brand-word { white-space: nowrap; }   /* one flex item, so the gap cannot split the name */

/* THE WORDMARK DOES NOT REACT TO HOVER, matching p3mai.com, whose logo changes
   nothing at all when hovered (measured, not assumed).

   Without this the generic `a:hover` rule caught it and turned "Web" into
   --color-accent-text (#8c6b15) while "Sandpit" stayed --color-accent
   (#c9a227). The result was two nearly-identical golds and a wordmark that
   lost its two-tone structure at the moment a visitor pointed at it -- the one
   moment it is being looked at directly. */
.brand:hover, .brand:focus { color: var(--color-primary); }
.brand:hover .brand-sandpit, .brand:focus .brand-sandpit { color: var(--color-accent); }
.brand-sandpit { color: var(--color-accent); }
.nav-list {
  display: flex;
  flex-wrap: wrap;
  /* 8px, matching p3mai.com's .nav-links exactly. This was 16px/24px -- three
     times p3mai's column gap -- which is what made the same six pills read as
     a much airier, looser bar than the one next door. */
  gap: 8px;
  list-style: none;
  padding: 0;
}
/* Pill buttons, matching p3mai.com's nav geometry exactly — inline-block,
   8px/16px padding, fully rounded, weight 500. Only the colours differ,
   because this header is navy where p3mai.com's is light: the same shapes in
   each site's own palette, rather than two different navigation designs. */
.nav-list a {
  display: inline-block;
  padding: 8px 16px;
  border-radius: 999px;
  font-weight: 500;
  color: var(--color-n700);
  text-decoration: none;
  transition: background 0.25s ease, color 0.25s ease;
}
.nav-list a:hover {
  background: var(--color-n100);
  color: var(--color-primary);
}
/* The current page is a FILLED DARK pill, exactly as p3mai.com marks its own --
   navy background, white text, no underline.

   It used to be a GOLD pill with an underline, and the comment here argued the
   underline was needed because colour alone is not enough for anyone who cannot
   distinguish it. That reasoning is sound and it does not apply: the
   distinction is not colour, it is a FILLED BACKGROUND against unfilled
   siblings, which is a shape difference visible without colour perception at
   all. The underline was belt on top of braces, and it made the bar busier than
   the one it is meant to match.

   Navy on white also outperforms the gold it replaces: 14.6:1 against 6.4:1. */
.nav-list a[aria-current="page"] {
  background: var(--color-primary);
  color: var(--color-n000);
}
.nav-list a[aria-current="page"]:hover {
  background: var(--color-primary);
  color: var(--color-n000);
}

/* The route back to p3mai.com. Outlined rather than filled, and set apart from
   the nav pills, because it LEAVES this site — a link that looks identical to
   the page tabs beside it claims to be one of them. The arrow does the same job
   again for anyone who reads shapes before they read words.
   Same pill geometry as the nav so it belongs to the bar rather than sitting on
   top of it, and white text on navy is 12.5:1. */
/* Identical to a .nav-list pill -- same padding, radius, weight, colour and
   hover. It used to carry an outline and 1px less padding to mark it as leaving
   the site; that made it the loudest control in the bar rather than the
   quietest. Same geometry as everything else now. */
.header-out {
  display: inline-block;
  padding: 8px 16px;
  border-radius: 999px;
  font-weight: 500;
  color: var(--color-n700);
  text-decoration: none;
  white-space: nowrap;
  transition: background 0.25s ease, color 0.25s ease;
}
.header-out:hover {
  background: var(--color-n100);
  color: var(--color-primary);
}

/* ---- Layout ---------------------------------------------------------------
   One idea per section with room around it (§3). The generous vertical scale
   IS the design here; crowding these would undo most of it. */
.section { padding: var(--sp-5) var(--sp-3); }
/* The hero already ends with generous space. Without this, the hero's bottom
   padding and the next section's top padding stack into roughly 200px of
   nothing, which on a text-only page looks like a rendering fault. */
.hero + .section { padding-top: var(--sp-3); }
.section > .inner { max-width: var(--container); margin: 0 auto; }
.section-tint { background: var(--color-n100); }
.section-dark { background: var(--color-primary); }
.section-dark h2, .section-dark h3 { color: var(--color-n000); }
.section-dark p, .section-dark li { color: var(--color-n100); }

/* The opening line of a section — one step larger than body text, so a reader
   skimming headings still picks up what the section is about. */
.lede { font-size: var(--fs-md); color: var(--color-n700); }
.section h2 + .lede { margin-top: var(--sp-2); }

/* ---- Hero -----------------------------------------------------------------
   Left-aligned, not centred: centred text at this length forces the reader to
   re-find the left edge on every line. */
/* NAVY, directly under the header -- p3mai.com's treatment, where every page
   opens on a navy band before any white section begins. This was on the page
   background, so the header sat on light and the page opened on light and there
   was no band at all.

   Same gradient as p3mai.com's .page-hero: 135deg, primary to primary-dark.

   The photograph STAYS. p3mai.com's hero carries no image, but this site's
   section-imagery rule puts a faded photo in the hero and dropping it to copy
   p3mai would lose more than it gained. It sits behind the gradient, masked in
   from the right as before, and now reads as texture in the navy rather than as
   a picture on white. */
.hero {
  padding: var(--sp-5) var(--sp-3) var(--sp-4);
  background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-dk) 100%);
  color: var(--color-n000);
}
.hero h1 { color: var(--color-n000); }
/* 0.85 white, matching p3mai.com's .page-hero p exactly. */
.hero p, .hero .lede { color: rgba(255, 255, 255, 0.85); }
/* The image now sits UNDER a dark ground, so it must not lighten it back up.
   Multiply keeps the navy dominant and lets the photo read as texture. */
.hero .section-media { opacity: 0.5; mix-blend-mode: luminosity; }
.hero .inner { max-width: var(--container); margin: 0 auto; }
/* max-width in em, NOT ch (AG-142).
   `ch` is the width of the font's own `0` glyph, so a box measured in ch
   CHANGES SIZE when the font changes -- the text does not merely re-wrap, the
   container it wraps inside moves. Poppins 600's zero is 0.6460 em and Segoe
   UI's is 0.5552, so this heading's box was 413px while the font loaded and
   454px afterwards, and everything below it jumped 47px.

   No amount of font-metric matching can fix that. `size-adjust` scales a face
   by its MEAN advance -- 8.96% for this pair -- while the `0` glyph alone
   differs by 16.4%. One multiplier cannot satisfy both, so a ch-sized box is
   unmatched by construction.

   11.63em is 18ch in Poppins 600 (18 x 0.6460), so the layout is unchanged
   from the design intent -- but `em` depends only on font-SIZE, which does not
   change when the family swaps. */
.hero h1 { font-size: var(--fs-2xl); max-width: 11.63em; }
/* Colour deliberately NOT set here. The hero is navy now, and this rule sat
   further down the file than the one that whitens it -- same specificity,
   later wins -- so it quietly put dark grey body text on a dark navy band.
   Size and spacing only. */
.hero p { margin-top: var(--sp-3); font-size: var(--fs-md); }
.hero .actions { margin-top: var(--sp-4); }

/* ---- Buttons --------------------------------------------------------------
   Text is n900 on gold, not white: white on #c9a227 is 1.9:1 and fails badly. */
/* A row of buttons that wraps to a second line rather than shrinking them or
   pushing off screen — which is what happens at 360px with two buttons. */
.actions { display: flex; flex-wrap: wrap; gap: var(--sp-2); align-items: center; }
/* A button row directly after a paragraph needs air above it. Without this the
   call to action sits at the paragraph's own line spacing and reads as part of
   the sentence rather than as the next thing to do — noticed on the portal page,
   where "Open the example portal" crowded the paragraph above it. Scoped to the
   paragraph-then-buttons case, so rows that follow a heading or a figure keep
   whatever spacing they already had. */
p + .actions { margin-top: var(--sp-3); }
/* Buttons. One filled style for the action we want, one outlined for the
   alternative — never two filled buttons competing in the same row. */
.button {
  display: inline-block;
  background: var(--color-accent);
  color: var(--color-n900);
  padding: var(--sp-1) var(--sp-3);
  border: 2px solid var(--color-accent);
  border-radius: var(--radius);
  text-decoration: none;
  font-weight: 600;
}
.button:hover { filter: brightness(1.08); color: var(--color-n900); }
.button-quiet {
  background: transparent;
  border-color: var(--color-n400);
  color: var(--color-primary);
}
.button-quiet:hover { border-color: var(--color-primary); filter: none; }
/* .hero is listed alongside .section-dark because the hero BECAME a dark
   section on 2026-08-13 and these rules did not follow it. The result was a
   live button -- "See what it costs" on the home page -- rendering navy text
   on the navy hero. Invisible.
   Any rule that answers "what happens on a dark ground" has to name .hero too;
   the hero is dark by gradient, not by carrying the .section-dark class. */
.section-dark .button-quiet,
.hero .button-quiet { color: var(--color-n000); border-color: var(--color-n400); }
.section-dark .button-quiet:hover,
.hero .button-quiet:hover { border-color: var(--color-n000); color: var(--color-n000); }

/* ---- Prose lists ----------------------------------------------------------
   Used for the "sounds familiar" triggers. No bullet glyph: these are short
   statements a reader recognises, not a specification. */
/* Separated by hairlines rather than bullets. These are statements a reader
   recognises one at a time, not items in a specification. */
.plain-list { list-style: none; padding: 0; margin-top: var(--sp-3); }
.plain-list li {
  max-width: var(--measure);
  padding: var(--sp-2) 0;
  border-top: 1px solid var(--color-n200);
  color: var(--color-n700);
}
.section-dark .plain-list li,
.hero .plain-list li { border-top-color: rgba(255, 255, 255, 0.18); }

/* EVERYTHING inside the hero has to answer to a dark ground, not just buttons.
   Making .hero navy on 2026-08-13 left the 404 page's recovery list -- Home,
   Process, Pricing, Contact -- as navy links and grey descriptions on navy.
   Eight elements at ratios between 1.00:1 and 1.48:1. Invisible, on the one
   page whose entire job is helping someone who is already lost.

   The lesson is not "fix the 404 page". It is that changing a section's ground
   changes every descendant that named a colour for the old one, and the ones
   that break are the pages nobody looks at. */
.hero li { color: rgba(255, 255, 255, 0.85); }
.hero a { color: var(--color-n000); text-decoration: underline; }
.hero a:hover { color: var(--color-accent); }
/* Except the buttons, which carry their own colours. */
.hero a.button { text-decoration: none; color: var(--color-n900); }
.hero a.button-quiet { color: var(--color-n000); }

/* ---- Cards ----------------------------------------------------------------
   Border rather than shadow. Shadows read as "app UI"; this needs to read as
   a document someone wrote. */
/* Card grids. Single column by default and multi-column only when there is
   room: building the other way round means writing rules to undo the desktop
   layout on every small screen. */
.grid { display: grid; gap: var(--sp-3); margin-top: var(--sp-4); }
@media (min-width: 720px) { .grid-3 { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 720px) { .grid-2 { grid-template-columns: repeat(2, 1fr); } }
.card {
  padding: var(--sp-3);
  background: var(--color-n000);
  border: 1px solid var(--color-n200);
  border-radius: var(--radius);
  /* Stretch + auto margin equalises heights without reserving a fixed one.
     A min-height here once left a blank line under five of six titles. */
  display: flex;
  flex-direction: column;
}
.card p { color: var(--color-n700); margin-top: var(--sp-2); }
.card .card-cta { margin-top: auto; padding-top: var(--sp-3); }
.card-step {
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--color-accent-text);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

/* ---- The five acts (process.html) -----------------------------------------
   Scroll-driven reveal, chosen over autoplay so the reader keeps control and
   WCAG 2.2.2 (pause/stop/hide) never applies — nothing moves on its own.

   The animated state is opt-in from JavaScript: acts are VISIBLE by default,
   and `.is-animated` is only added when JS runs AND the reader has not asked
   for reduced motion. So no-JS and reduced-motion both get the plain page
   rather than a blank one. Transform and opacity only, so nothing reflows and
   the animation cannot cost us CLS. */
/* The spine. A faint full-height track with a gold fill drawn down it as the
   reader descends, and a numbered node per stage sitting on the line. This is
   what makes the page read as a SEQUENCE rather than five paragraphs that
   happen to be numbered.

   The fill's height is the only thing JavaScript touches, and it is set through
   the CSSOM — an inline style attribute would be blocked by style-src 'self'.
   Its CSS default is the FULL height, so with JavaScript off or reduced motion
   requested the line is simply complete, which is the correct resting state
   rather than an empty track. */
.acts {
  position: relative;
  margin-top: var(--sp-4);
  padding-left: 2.5rem;
}
.acts::before {
  content: "";
  position: absolute;
  left: 0.875rem;
  top: 0;
  bottom: 0;
  width: 2px;
  background: var(--color-n200);
}
.acts-progress {
  position: absolute;
  left: 0.875rem;
  top: 0;
  width: 2px;
  height: 100%;
  background: var(--color-accent);
}

.act {
  position: relative;
  padding: var(--sp-4) 0;
  border-top: 1px solid var(--color-n200);
}
.act:first-child { border-top: 0; padding-top: var(--sp-2); }
.act-head { display: flex; align-items: baseline; gap: var(--sp-2); flex-wrap: wrap; }

/* The number becomes a node ON the spine. Absolute, so it sits on the line at
   any breakpoint without the two-column grid below having to know about it. */
.act-num {
  position: absolute;
  left: -2.5rem;
  top: var(--sp-4);
  display: grid;
  place-items: center;
  width: 1.75rem;
  height: 1.75rem;
  border-radius: 50%;
  border: 2px solid var(--color-n200);
  background: var(--color-n000);
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--color-n700);
  font-variant-numeric: tabular-nums;
  transition: background 0.35s ease-out 0.12s, border-color 0.35s ease-out 0.12s,
              color 0.35s ease-out 0.12s;
}
.act:first-child .act-num { top: var(--sp-2); }
/* Filled once the reader reaches this stage. Colour is not the only signal —
   the node also gains a solid ring, so the state survives a monochrome view. */
.act.is-in .act-num,
.acts:not(.is-animated) .act .act-num {
  background: var(--color-accent);
  border-color: var(--color-accent);
  color: var(--color-n900);
}
.act-stages {
  margin-top: var(--sp-2);
  font-size: var(--fs-sm);
  color: var(--color-n700);
  letter-spacing: 0.02em;
  text-transform: uppercase;
}
.act-body { margin-top: var(--sp-2); }
/* The differentiator is the point of the page, so it is set apart rather than
   left as another paragraph the eye can skim past. */
.act-diff, .callout {
  margin-top: var(--sp-3);
  padding: var(--sp-2) var(--sp-3);
  border-left: 4px solid var(--color-accent);
  background: var(--color-n100);
  max-width: var(--measure);
}
.act-diff strong, .callout strong { color: var(--color-primary); }

.acts.is-animated .act {
  opacity: 0;
  transform: translateY(14px);
  transition: opacity 0.5s ease-out, transform 0.5s ease-out;
}
.acts.is-animated .act.is-in { opacity: 1; transform: none; }
.acts.is-animated .act-diff {
  opacity: 0;
  transform: translateY(8px);
  transition: opacity 0.45s ease-out 0.2s, transform 0.45s ease-out 0.2s;
}
.acts.is-animated .act.is-in .act-diff { opacity: 1; transform: none; }

/* On a wide screen the acts become two columns: the stage name on the left,
   what happens on the right. Single-column, this page was five narrow text
   blocks against an empty right half — which reads as unfinished rather than
   restrained, and restraint is the whole instruction. The measure stays intact
   because the right column is still capped; the width is used by the LAYOUT
   rather than by longer lines. */
@media (min-width: 900px) {
  .act {
    display: grid;
    grid-template-columns: 15rem 1fr;
    /* auto/1fr, not auto/auto: without this the two left-hand rows share the
       height of the spanning right column and the stage label floats away
       from the title it labels. */
    grid-template-rows: auto 1fr;
    column-gap: var(--sp-5);
  }
  .act-head { grid-column: 1; grid-row: 1; }
  .act-stages { grid-column: 1; grid-row: 2; margin-top: var(--sp-1); }
  .act-body { grid-column: 2; grid-row: 1 / span 2; margin-top: 0; }
  .act-body p { max-width: 40rem; }
  .act-diff, .callout { max-width: 40rem; }
}

/* ---- Screenshots ----------------------------------------------------------
   A bordered frame, because an unframed screenshot of a web page sitting on a
   web page reads as part of THIS page rather than a picture of another one.
   The caption is not decoration: it states what the image is and what was taken
   out of it, on a site whose whole argument is that evidence carries
   provenance. */
.shot { margin-top: var(--sp-4); }
.shot img {
  display: block;
  width: 100%;
  border: 1px solid var(--color-n200);
  border-radius: var(--radius);
}
/* A quiet note in body copy. NOT .footer-note, which is coloured for the dark
   footer: on white that is roughly 2.4:1 and fails AA. Borrowing a class across
   backgrounds is how a contrast failure gets shipped looking deliberate. */
.note { font-size: var(--fs-sm); color: var(--color-n700); }

.shot figcaption {
  margin-top: var(--sp-2);
  font-size: var(--fs-sm);
  color: var(--color-n700);
  max-width: var(--measure);
}

/* ---- Pricing --------------------------------------------------------------
   Prices in the open, both currencies, first-year total stated (§3, AG-33). */
/* Wide tables scroll INSIDE this wrapper. Without it the table forces the
   whole page to scroll sideways, which is both unpleasant and a launch-checklist
   failure. The min-width below is what makes the columns readable rather than
   crushed; the wrapper is what keeps that from leaking out to the page. */
.table-wrap { overflow-x: auto; margin-top: var(--sp-4); }
table { border-collapse: collapse; width: 100%; min-width: 42rem; }
caption { text-align: left; padding-bottom: var(--sp-2); color: var(--color-n700); }
th, td { text-align: left; padding: var(--sp-2); border-bottom: 1px solid var(--color-n200); }
th { font-weight: 600; color: var(--color-primary); }
td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; }
tr.is-default td, tr.is-default th { background: var(--color-n100); }

/* ---- Forms ----------------------------------------------------------------
   Labels are real <label for> elements and the radio group is a <fieldset>
   with a <legend>: a screen reader otherwise announces five options with no
   idea what question they answer. */
/* Forms are capped at reading width. A full-width text field on a wide screen
   looks like it wants an essay, and people write less when the box is huge. */
.form { margin-top: var(--sp-4); max-width: var(--measure); }
.field { margin-bottom: var(--sp-3); }
.field label, .field legend { display: block; font-weight: 600; margin-bottom: var(--sp-1); }
.field .hint { font-size: var(--fs-sm); color: var(--color-n700); margin-bottom: var(--sp-1); }
input[type="text"], input[type="email"], input[type="tel"], textarea {
  width: 100%;
  font: inherit;
  padding: var(--sp-1);
  border: 1px solid var(--color-n400);
  border-radius: var(--radius);
  background: var(--color-n000);
  color: var(--color-n900);
}
textarea { min-height: 7rem; resize: vertical; }
fieldset { border: 1px solid var(--color-n200); border-radius: var(--radius); padding: var(--sp-2); }
.choices { display: flex; flex-wrap: wrap; gap: var(--sp-1) var(--sp-3); margin-top: var(--sp-1); }
.choice { display: flex; align-items: center; gap: var(--sp-1); font-weight: 400; }
button.button { font: inherit; cursor: pointer; }
/* Honeypot: hidden by class, never by an inline style. Formspree's own docs
   suggest style="display:none", which this site's CSP blocks silently. */
.hp { position: absolute; left: -9999px; }
.form-status { margin-top: var(--sp-2); font-weight: 600; }
.form-status.is-error { color: #a3231d; }

/* ---- Footer ---------------------------------------------------------------- */
/* Footer. Darker than the header so the page has a clear bottom edge rather
   than appearing to run out. */
.site-footer {
  padding: var(--sp-4) var(--sp-3);
  font-size: var(--fs-sm);
  color: var(--color-n100);
  background: var(--color-primary-dk);
}
.site-footer .inner { max-width: var(--container); margin: 0 auto; }
.site-footer a { color: var(--color-n000); }
.site-footer p { max-width: none; }
.footer-note { margin-top: var(--sp-2); color: var(--color-n400); }

/* ---- Wider screens --------------------------------------------------------- */
@media (min-width: 720px) {
  .section { padding: var(--sp-6) var(--sp-4); }
  .hero { padding: var(--sp-6) var(--sp-4) var(--sp-5); }
  h1 { font-size: var(--fs-2xl); }
  h2 { font-size: var(--fs-xl); }
}

/* ---- Decorative section imagery -------------------------------------------
   Used by the `section-imagery` skill. Add `has-media` to a section and put an
   <img class="section-media" alt="" loading="lazy"> as its first child; the
   image bleeds across the section and fades in from the left, staying slightly
   transparent at the right so it reads as part of the page rather than a
   photograph pasted onto it.

   alt="" and nothing else: this is decoration. It carries nothing the copy
   does not already say, so announcing it to a screen reader is noise.
   z-index -1 with isolation on the parent keeps it behind the text without
   escaping the section and sliding under the header.

   ⚠️ THE STOP POSITIONS BELOW ARE A STARTING POINT, NOT A SETTING.
   They decide where the photograph is strong, and therefore whether the text
   over it is still readable — which depends entirely on where this client's
   copy sits. A centred column and a left-aligned one need different curves.
   Neither pa11y nor Lighthouse can check this: both read CSS colour values, so
   text on an image is invisible to them and they will report zero issues on a
   page that genuinely fails. After setting an image, run:

       node tools/audit-image-contrast.mjs --dir site

   and tune these stops until it passes at every width. */
.has-media { position: relative; overflow: hidden; isolation: isolate; }
.has-media > * { position: relative; }
.section-media {
  position: absolute; inset: 0; z-index: -1;
  width: 100%; height: 100%;
  object-fit: cover; object-position: center right;
  -webkit-mask-image: linear-gradient(to right, transparent 0%,
    rgba(0, 0, 0, 0.04) 40%, rgba(0, 0, 0, 0.10) 65%,
    rgba(0, 0, 0, 0.22) 78%, rgba(0, 0, 0, 0.85) 100%);
  mask-image: linear-gradient(to right, transparent 0%,
    rgba(0, 0, 0, 0.04) 40%, rgba(0, 0, 0, 0.10) 65%,
    rgba(0, 0, 0, 0.22) 78%, rgba(0, 0, 0, 0.85) 100%);
}
@media (max-width: 760px) {
  /* Copy runs the full width on a phone, so the desktop curve would put the
     photograph directly under body text. Drop the peak to a wash. Most
     visitors arrive on a phone, so this is the case that has to be right. */
  .section-media {
    -webkit-mask-image: linear-gradient(to right, transparent 0%,
      rgba(0, 0, 0, 0.05) 55%, rgba(0, 0, 0, 0.12) 100%);
    mask-image: linear-gradient(to right, transparent 0%,
      rgba(0, 0, 0, 0.05) 55%, rgba(0, 0, 0, 0.12) 100%);
  }
}

/* Respect user motion preference for any animation added later */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation: none !important; transition: none !important; }
}


/* The off-canvas menu panel is position:fixed and sits a panel-width to the
   RIGHT of the viewport until it is opened. Chromium counts a fixed element in
   the document's scrollable overflow, so without this every page scrolls
   sideways on a phone -- which is exactly what the cross-browser gate caught,
   on all six pages at once.

   BOTH html and body, not just html: clipping only the root leaves the body
   able to scroll the page in Chromium. WebKit needs neither, so a machine with
   no WebKit engine cannot tell you whether you got this right. p3mai.com
   carries the identical pair for the identical reason. */
html { overflow-x: clip; }
body { overflow-x: clip; }

/* ---- Mobile navigation ---------------------------------------------------
   The header links fold into a three-bar menu below 700px, the same behaviour
   and the same breakpoint as p3mai.com. Consistency is the point: a visitor
   who has used one of these sites should not have to learn a second set of
   rules.

   The bars are currentColor, so the button takes the header's own text colour
   instead of hard-coding one per site. */
.nav-toggle {
  display: none;
  flex-direction: column;
  gap: 5px;
  padding: 8px;
  margin-left: auto;
  background: none;
  border: 0;
  cursor: pointer;
  color: var(--color-primary);
  z-index: 110;
}
.nav-toggle span {
  display: block;
  width: 26px;
  height: 3px;
  border-radius: 2px;
  background: currentColor;
  transition: transform 0.25s ease, opacity 0.25s ease;
}
/* Open state: the outer bars rotate into an X and the middle one fades. The
   translate has to come FIRST in the transform list -- rotating then moving
   moves along the rotated axis and the bars miss each other. */
.nav-toggle.open span:nth-child(1) { transform: translateY(8px) rotate(45deg); }
.nav-toggle.open span:nth-child(2) { opacity: 0; }
.nav-toggle.open span:nth-child(3) { transform: translateY(-8px) rotate(-45deg); }

@media (max-width: 700px) {
  .nav-toggle { display: flex; }
  [data-nav-panel] {
    position: fixed;
    top: 0;
    right: 0;
    height: 100vh;
    width: min(78%, 320px);
    background: #ffffff;
    flex-direction: column;
    align-items: flex-start;
    padding: 92px 28px 40px;
    gap: 6px;
    box-shadow: 0 12px 40px rgba(0, 0, 0, 0.28);
    /* Off-canvas rather than display:none, so it slides rather than appearing,
       and so the links stay in the accessibility tree in a predictable place. */
    transform: translateX(100%);
    transition: transform 0.25s ease;
    overflow-y: auto;
    z-index: 105;
  }
  [data-nav-panel].open { transform: translateX(0); }
  [data-nav-panel] li { width: 100%; }
  [data-nav-panel] li a { display: block; width: 100%; }
}

/* Someone who has asked their system for less motion gets the panel without
   the slide, and the bars without the rotation. It still works; it just does
   not move. */
@media (prefers-reduced-motion: reduce) {
  .nav-toggle span, [data-nav-panel] { transition: none; }
}

/* ---- The project enquiry form -------------------------------------------
   A long form, deliberately on one scroll rather than in steps. Steps need
   JavaScript, hold state, and give a visitor somewhere to get stuck; a single
   page with four honest headings does the same work with none of that, and it
   still works with the script switched off. */
.form-long .form-part {
  margin: var(--sp-5) 0 var(--sp-1);
  padding-top: var(--sp-3);
  border-top: 2px solid var(--color-n200);
  font-size: 1.15rem;
  color: var(--color-primary);
}
.form-long .form-part:first-of-type { margin-top: 0; border-top: 0; padding-top: 0; }
/* The four questions we cannot build without. Marked in WORDS, not with an
   asterisk and a colour: an asterisk is a convention people have to already
   know, and colour alone is not a signal WCAG 2.2 accepts. */
.req {
  font-size: 0.75rem; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.06em; color: var(--color-accent-text);
  margin-left: 0.4rem;
}
.field textarea.tall { min-height: 9rem; }

/* Choice cards. Radios dressed as cards, and the radio STAYS VISIBLE --
   hiding it and faking the tick puts the selected state on colour alone.
   `label.choice`, not `.choice`: these cards are labels inside a .field, and
   `.field label` (one class plus an element) outranks a bare class. That bit
   us on the ProtoSandpit form -- grid-template-columns applied while display
   did not, which is the tell that something more specific is winning. */
.choice-set { border: 0; padding: 0; margin: 0 0 var(--sp-3); }
.choice-set legend { font-weight: 700; padding: 0; }
label.choice {
  display: grid; grid-template-columns: auto 1fr; gap: 2px 12px;
  align-items: start; padding: 12px 14px; margin-bottom: 8px;
  background: var(--color-n000); border: 1px solid var(--color-n200);
  border-radius: var(--radius-sm); cursor: pointer;
}
label.choice:hover { border-color: var(--color-primary); }
label.choice input { grid-row: 1 / span 2; margin: 3px 0 0; width: 18px; height: 18px;
  accent-color: var(--color-primary); }
.choice-label { font-weight: 700; }
.choice-hint { grid-column: 2; color: var(--color-n700); font-size: 0.9rem; }
label.choice:has(input:checked) {
  border-color: var(--color-primary); border-width: 2px;
  background: var(--color-n100); padding: 11px 13px;
}
label.choice:has(input:focus-visible) { outline: 3px solid var(--color-primary); outline-offset: 2px; }

@media (min-width: 700px) {
  /* Two columns once there is room. Six stacked cards is a long scroll for a
     question that is meant to be answered at a glance. */
  .choice-set { display: grid; grid-template-columns: 1fr 1fr; gap: 0 12px; }
  .choice-set legend, .choice-set > .hint { grid-column: 1 / -1; }
}

/* ---- Question blocks on the project enquiry form -------------------------
   LIFTED VERBATIM from the client portal's stylesheet. Douglas asked for the
   public form to look identical to the portal's requirements questions, and
   identical is not something two hand-written implementations stay -- so this
   is the portal's rules with only the token names remapped to this sheet's
   scale. If either changes, change both; the portal's copy is the original.

   The icon tile and the "3 of 19" chip are written in by js/questions.js, which
   is itself lifted from portal.js. Cosmetic, but it is what makes a long form
   read as a sequence with an end rather than a wall. */
.q { border: 1px solid var(--color-n100); border-radius: 6px; padding: var(--sp-2); margin-bottom: var(--sp-2); }
.q-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.6rem; }
.q-icon {
  width: 2.3rem; height: 2.3rem; border-radius: 8px; flex: none;
  background: rgba(201, 162, 39, 0.16); color: var(--color-primary);
  display: flex; align-items: center; justify-content: center;
}
.q-icon svg { width: 1.35rem; height: 1.35rem; fill: none; stroke: currentColor;
  stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.q-chip { background: var(--color-accent); color: var(--color-n900); font-size: 0.7rem;
  font-weight: 700; padding: 0.2rem 0.65rem; border-radius: 999px; letter-spacing: 0.03em; }
.q label, .q legend { font-weight: 700; color: var(--color-primary); display: block; }
.q .why {
  font-size: 0.85rem; color: var(--color-n700); margin: 0.3rem 0 var(--sp-1);
  padding-left: 0.7rem; border-left: 2px solid var(--color-n400);
}
.q .why-label { font-weight: 700; font-style: italic; }
.q input[type="text"], .q input[type="email"], .q input[type="tel"], .q textarea, .q select {
  width: 100%; padding: 0.6rem; border: 1px solid var(--color-n400);
  border-radius: 4px; font: inherit;
}
.q textarea { min-height: 5.5rem; }
.q textarea.tall { min-height: 9rem; }
.q textarea::placeholder, .q input::placeholder { color: var(--color-n400); font-style: italic; opacity: 1; }
/* The choice groups are questions too, so they carry the same frame. */
fieldset.q { margin: 0 0 var(--sp-2); }
