/* =============================================================================
   Apex Custom Coding: shared stylesheet
   -----------------------------------------------------------------------------
   One file, loaded by every page. No framework, no build step, no runtime cost
   beyond a single cached request.

   Order matters, do not shuffle:
     1. Tokens       : the only place raw values are allowed
     2. Reset
     3. Base / typography
     4. Layout primitives
     5. Site header + navigation
     6. Site footer
     7. Components
     8. Utilities
     9. Motion + print
   Anything reused twice belongs in 7. Anything one page needs alone does not
   belong in this file at all.
============================================================================= */


/* 1. TOKENS ================================================================ */

:root {
  /* -- Colour ---------------------------------------------------------------
     Single dark theme for v1. Every pairing below is >= 4.5:1 against its
     intended background; if you add a colour, verify it before shipping. */
  --c-bg:          #0B0D11;
  --c-bg-sunk:     #07090C;
  --c-surface:     #11141A;
  --c-surface-2:   #171B23;
  --c-line:        #232935;
  --c-line-soft:   #1A1F28;

  --c-text:        #E8EBF1;   /* 15.9:1 on --c-bg */
  --c-text-dim:    #A6AEBD;   /*  8.6:1 */
  --c-text-faint:  #757E90;   /*  4.8:1, smallest permitted text colour */

  --c-accent:      #5B8CFF;   /*  6.2:1 */
  --c-accent-hot:  #86A9FF;
  --c-accent-sunk: #16233F;
  --c-signal:      #3DD68C;   /* affirmative: delivered, accepted, no-risk */
  --c-warn:        #FFB454;
  --c-error:       #FF8B7A;   /*  8.8:1 on --c-bg-sunk, verified, not eyeballed */

  --c-on-accent:   #07090C;   /*  6.4:1 on --c-accent */

  /* -- Type -----------------------------------------------------------------
     System stack: zero network cost, renders instantly, no FOUT. Swap for a
     self-hosted variable font only if brand identity demands it, and then
     preload it, or the speed advantage is gone. */
  --font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
               "Helvetica Neue", Arial, sans-serif;

  /* Fluid modular scale, ratio ~1.2 mobile -> ~1.28 desktop, 360px..1440px. */
  --text-xs:  clamp(0.78rem, 0.76rem + 0.08vw, 0.83rem);
  --text-sm:  clamp(0.88rem, 0.85rem + 0.12vw, 0.94rem);
  --text-base:clamp(1.00rem, 0.97rem + 0.15vw, 1.08rem);
  --text-md:  clamp(1.13rem, 1.07rem + 0.28vw, 1.28rem);
  --text-lg:  clamp(1.35rem, 1.24rem + 0.50vw, 1.65rem);
  --text-xl:  clamp(1.65rem, 1.44rem + 0.92vw, 2.20rem);
  --text-2xl: clamp(2.00rem, 1.64rem + 1.58vw, 2.95rem);
  --text-3xl: clamp(2.40rem, 1.83rem + 2.50vw, 3.90rem);

  --leading-tight: 1.08;
  --leading-snug:  1.28;
  --leading-body:  1.65;

  --tracking-tight: -0.022em;
  --tracking-wide:   0.14em;

  --weight-normal: 400;
  --weight-medium: 500;
  --weight-bold:   700;

  /* -- Space ----------------------------------------------------------------
     4px base. Use tokens, never raw rem, so vertical rhythm stays uniform. */
  --s-1:  0.25rem;
  --s-2:  0.5rem;
  --s-3:  0.75rem;
  --s-4:  1rem;
  --s-5:  1.5rem;
  --s-6:  2rem;
  --s-7:  3rem;
  --s-8:  4rem;
  --s-9:  6rem;
  --s-10: 8rem;

  /* Section rhythm scales with viewport so short pages do not feel cramped. */
  --section-y:      clamp(3.5rem, 2.2rem + 5.5vw, 7rem);
  --section-y-lg:   clamp(5rem, 3rem + 8vw, 10rem);

  /* -- Layout --------------------------------------------------------------- */
  --width-page:  1560px;
  --width-text:  68ch;   /* readable measure, never let body copy exceed this */
  --gutter:      clamp(1.25rem, 0.9rem + 1.6vw, 2.5rem);
  /* Header and brand scale together off one source of truth, so the mark can
     never go back to being a fixed-pixel outlier while everything else is fluid. */
  --header-h:    clamp(4.25rem, 5.4vw, 5.5rem);
  --brand-mark:  clamp(2rem, 2.9vw, 3rem);

  /* -- Form ----------------------------------------------------------------- */
  --radius-sm: 6px;
  --radius:    10px;
  --radius-lg: 16px;
  --radius-pill: 999px;

  --border: 1px solid var(--c-line);

  --shadow-sm: 0 1px 2px rgb(0 0 0 / 0.4);
  --shadow:    0 8px 24px -8px rgb(0 0 0 / 0.55);
  --shadow-lg: 0 24px 60px -20px rgb(0 0 0 / 0.7);
  --glow:      0 0 0 1px var(--c-accent-sunk), 0 12px 40px -12px rgb(91 140 255 / 0.45);

  --ease:      cubic-bezier(0.4, 0, 0.2, 1);
  --ease-out:  cubic-bezier(0.16, 1, 0.3, 1);
  --dur-fast:  120ms;
  --dur:       220ms;
  --dur-slow:  420ms;

  --z-header:   100;
  --z-nav:      110;
  --z-skiplink: 200;

  color-scheme: dark;
}


/* 2. RESET ================================================================= */

*, *::before, *::after { box-sizing: border-box; }

* { margin: 0; }

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  scroll-padding-top: calc(var(--header-h) + var(--s-4));
}

body {
  min-height: 100svh;
  background-color: var(--c-bg);
  color: var(--c-text);
  font-family: var(--font-sans);
  font-size: var(--text-base);
  line-height: var(--leading-body);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-wrap: break-word;
}

/* Scroll lock for an open mobile nav. Taking the body out of flow is what makes
   this work in iOS Safari, where `overflow: hidden` alone does not. site.js
   captures scrollY, sets the matching negative `top` and restores it on close. */
body.is-nav-open {
  position: fixed;
  left: 0;
  right: 0;
  width: 100%;
  overflow: hidden;
}

/* Necessary consequence of the above. With the body out of flow and offset by
   -scrollY, `position: sticky` has nothing to stick to, so the header would fall
   back to its static position, off the top of the screen, leaving a strip of
   page visible above the panel. Pinning it keeps the lockup and the close button
   exactly where they were. */
body.is-nav-open .site-top {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
}

img, picture, svg, video, canvas {
  display: block;
  max-width: 100%;
  height: auto;
}

input, button, textarea, select { font: inherit; color: inherit; }

button { background: none; border: 0; cursor: pointer; }

ul[class], ol[class] { list-style: none; padding: 0; }

:where(a) { color: var(--c-accent); text-decoration-thickness: 1px; text-underline-offset: 0.18em; }
:where(a):hover { color: var(--c-accent-hot); }

/* One focus treatment everywhere. Never remove it, replace it if you must. */
:focus-visible {
  outline: 2px solid var(--c-accent-hot);
  outline-offset: 3px;
  border-radius: var(--radius-sm);
}
:focus:not(:focus-visible) { outline: none; }

::selection { background: var(--c-accent); color: var(--c-on-accent); }


/* 3. BASE / TYPOGRAPHY ===================================================== */

h1, h2, h3, h4 {
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-tight);
  line-height: var(--leading-tight);
  text-wrap: balance;
}

h1 { font-size: var(--text-3xl); }
h2 { font-size: var(--text-2xl); }
h3 { font-size: var(--text-lg); line-height: var(--leading-snug); }
h4 { font-size: var(--text-md); line-height: var(--leading-snug); }

p, li { text-wrap: pretty; }

p + p { margin-top: var(--s-4); }

small { font-size: var(--text-sm); }

/* No font-family here, and none in :root. A monospace face is available ONLY
   inside the styleguide, scoped below. On a customer-facing page a literal value
   is marked with this background chip instead.

   The token lives on .page--styleguide rather than :root deliberately. Being
   absent from a marketing page is a fact someone can undo; being unreachable from
   one is a property of the stylesheet. check-css-invariants.php fails the build if
   any monospace family becomes reachable from an unscoped selector. */
code {
  background: var(--c-surface-2);
  border: 1px solid var(--c-line-soft);
  border-radius: var(--radius-sm);
  padding: 0.12em 0.4em;
}

hr {
  border: 0;
  border-top: var(--border);
  margin-block: var(--s-7);
}


/* 4. LAYOUT PRIMITIVES ===================================================== */

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

.container--narrow { max-width: 780px; }

.site-main { display: block; }

.section { padding-block: var(--section-y); }
.section--lg { padding-block: var(--section-y-lg); }
.section--flush-top { padding-block-start: 0; }
.section--sunk { background: var(--c-bg-sunk); }
.section--surface { background: var(--c-surface); }

/* Bordered sections read as deliberate slabs rather than accidental colour shifts. */
.section--sunk, .section--surface {
  border-block: var(--border);
}

/* Vertical rhythm: one owner of the gap, no margin collapsing surprises. */
.stack > * + * { margin-top: var(--flow, var(--s-4)); }
.stack--tight  { --flow: var(--s-3); }
.stack--loose  { --flow: var(--s-6); }

/* Auto-fitting grid. Set --min per instance to control the breakpoint. */
.grid {
  display: grid;
  gap: var(--grid-gap, var(--s-5));
  grid-template-columns: repeat(auto-fit, minmax(min(var(--min, 260px), 100%), 1fr));
}
.grid--2 { --min: 320px; }
.grid--3 { --min: 260px; }
.grid--4 { --min: 200px; }

.cluster {
  display: flex;
  flex-wrap: wrap;
  gap: var(--cluster-gap, var(--s-3));
  align-items: center;
}

.measure { max-width: var(--width-text); }


/* 5. SITE HEADER + NAVIGATION ============================================== */

.skip-link {
  position: absolute;
  left: var(--s-4);
  top: var(--s-4);
  z-index: var(--z-skiplink);
  transform: translateY(-200%);
  background: var(--c-accent);
  color: var(--c-on-accent);
  font-weight: var(--weight-medium);
  padding: var(--s-2) var(--s-4);
  border-radius: var(--radius-sm);
  text-decoration: none;
  transition: transform var(--dur) var(--ease-out);
}
.skip-link:focus { transform: translateY(0); color: var(--c-on-accent); }

/* The strip and the header move as ONE sticky unit. Previously the strip scrolled
   away underneath a translucent blurred header, which sliced its text through the
   middle. That was the "clipped banner" defect, and it was a stacking problem,
   not a height problem. Explicit line-height and min-height guard the line box too. */
.site-top {
  position: sticky;
  top: 0;
  z-index: var(--z-header);
}

.prelaunch {
  line-height: 1.4;
  min-height: 2.25rem;
  /* Not flex. As a flex row the text became one full-width item that centred its
     own lines, which stranded the dot alone against the left edge as soon as the
     message wrapped to two lines on a phone. Inline flow keeps them together. */
  display: block;
  background: var(--c-accent-sunk);
  border-bottom: 1px solid var(--c-line);
  color: var(--c-text-dim);
  font-size: var(--text-xs);
  letter-spacing: 0.01em;
  padding: var(--s-2) var(--gutter);
  text-align: center;
}
.prelaunch__dot {
  display: inline-block;
  vertical-align: middle;
  width: 6px; height: 6px;
  margin-right: var(--s-2);
  border-radius: 50%;
  background: var(--c-warn);
}

.site-header {
  position: relative;          /* stickiness lives on .site-top now */
  border-bottom: 1px solid transparent;
  transition: border-color var(--dur) var(--ease);
}

/* The blur is on a pseudo-element, NOT on .site-header itself, and it must stay
   that way. `backdrop-filter` makes an element a containing block for
   position:fixed descendants, and the mobile nav panel IS a descendant of the
   header. With the filter on .site-header the panel resolved its inset against
   the header box instead of the viewport, so it opened as a sliver inside the
   header. Moving the filter down one level fixes it and keeps the effect. */
.site-header::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: color-mix(in srgb, var(--c-bg) 82%, transparent);
  backdrop-filter: saturate(160%) blur(12px);
  transition: background-color var(--dur) var(--ease);
}
/* Border only once content is behind it, a hairline over a hero looks like a seam. */
.site-header.is-scrolled { border-bottom-color: var(--c-line); }
.site-header.is-scrolled::before {
  background: color-mix(in srgb, var(--c-bg) 94%, transparent);
}

.site-header__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-5);
  min-height: var(--header-h);
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: clamp(0.6rem, 0.9vw, 1rem);
  color: var(--c-text);
  text-decoration: none;
  flex: none;
  min-width: 0;
}
.brand:hover { color: var(--c-text); }

/* Flat white. Blue stays a UI accent for buttons and links. It is not the brand. */
.brand__mark {
  width: var(--brand-mark);
  height: var(--brand-mark);
  color: #fff;
  flex: none;
  transition: transform var(--dur) var(--ease-out);
}
.brand:hover .brand__mark { transform: translateY(-2px); }

/* Horizontal lockup. Baselines aligned so the pair sits on the cap-height of
   "Apex" rather than on two unrelated bounding boxes. */
.brand__text {
  display: flex;
  align-items: baseline;
  gap: clamp(0.35rem, 0.6vw, 0.6rem);
  line-height: 1;
  white-space: nowrap;
}
.brand__name {
  font-size: clamp(1.15rem, 1.5vw, 1.6rem);
  font-weight: 800;
  letter-spacing: var(--tracking-tight);
}
/* Floored at 0.75rem, below that, wide-tracked caps turn to grey mush. */
.brand__sub {
  font-size: clamp(0.75rem, 0.78vw, 0.88rem);
  font-weight: var(--weight-medium);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--c-text-faint);
}

/* Below the nav breakpoint the sub-label would crowd the menu button. The mark
   and "Apex" still carry the brand; nothing shrinks below the legibility floor. */
@media (max-width: 560px) {
  .brand__sub { display: none; }
}

/* Footer: large and quiet, a sign-off, not a second header. */
.brand--footer .brand__mark {
  width: clamp(3rem, 6vw, 4.5rem);
  height: clamp(3rem, 6vw, 4.5rem);
  opacity: .55;
}
.brand--footer .brand__name { font-size: clamp(1.3rem, 1.7vw, 1.8rem); }

.nav-toggle {
  display: none;
  width: 44px; height: 44px;   /* full touch target, never shrink below this */
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-sm);
  color: var(--c-text);
  flex: none;
}
.nav-toggle__bars { display: grid; gap: 5px; width: 20px; }
.nav-toggle__bars span {
  display: block;
  height: 2px;
  background: currentColor;
  border-radius: 2px;
  transition: transform var(--dur) var(--ease-out), opacity var(--dur-fast) var(--ease);
}
.nav-toggle[aria-expanded="true"] .nav-toggle__bars span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
.nav-toggle[aria-expanded="true"] .nav-toggle__bars span:nth-child(2) { opacity: 0; }
.nav-toggle[aria-expanded="true"] .nav-toggle__bars span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }

.site-nav {
  display: flex;
  align-items: center;
  gap: var(--s-5);
}

.site-nav__list {
  display: flex;
  align-items: center;
  gap: var(--s-1);
  margin: 0;
}

.site-nav__link {
  display: block;
  position: relative;
  padding: var(--s-2) var(--s-3);
  border-radius: var(--radius-sm);
  color: var(--c-text-dim);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  text-decoration: none;
  white-space: nowrap;
  transition: color var(--dur-fast) var(--ease), background-color var(--dur-fast) var(--ease);
}
.site-nav__link:hover { color: var(--c-text); background: var(--c-surface-2); }
.site-nav__link.is-current { color: var(--c-text); }
.site-nav__link.is-current::after {
  content: "";
  position: absolute;
  left: var(--s-3);
  right: var(--s-3);
  bottom: 2px;
  height: 2px;
  border-radius: 2px;
  background: var(--c-accent);
}

/* -- Mobile navigation ------------------------------------------------------
   Collapse rules are scoped to .js: with scripting off the nav stays a plain
   visible list rather than a panel with no way to open it. */
/* 1100, not 900, and the number is measured rather than chosen. The inline nav is
   809px wide and with the logo beside it the header needs 1092px to fit. At 900 the
   drawer switched off 192px too early, so EVERY page scrolled sideways by up to
   90px from 901px to 1091px, which is iPad landscape and every small laptop. It was
   invisible because nobody had ever tested a width in that band.

   MOBILE_QUERY in assets/js/site.js must carry the same number. If they disagree
   the burger and the drawer logic disagree, which is a worse bug than this one. */
@media (max-width: 1100px) {
  .js-ready .nav-toggle { display: inline-flex; }

  .js-ready .site-nav {
    position: fixed;
    inset: var(--nav-top, var(--header-h)) 0 0 0;
    z-index: var(--z-nav);
    flex-direction: column;
    align-items: stretch;
    justify-content: flex-start;
    gap: var(--s-2);
    padding: var(--s-5) var(--gutter) var(--s-8);
    background: var(--c-bg);
    border-top: var(--border);
    overflow-y: auto;
    /* Stops the scroll from chaining through to the locked page behind it. */
    overscroll-behavior: contain;
    opacity: 0;
    visibility: hidden;
    transform: translateY(-8px);
    transition: opacity var(--dur) var(--ease),
                transform var(--dur) var(--ease-out),
                visibility 0s linear var(--dur);
  }
  .js-ready .site-nav.is-open {
    opacity: 1;
    visibility: visible;
    transform: none;
    transition-delay: 0s;
  }

  .js-ready .site-nav__list { flex-direction: column; align-items: stretch; gap: 0; }
  .js-ready .site-nav__link {
    padding: var(--s-4) var(--s-2);
    font-size: var(--text-md);
    border-bottom: 1px solid var(--c-line-soft);
    border-radius: 0;
  }
  .js-ready .site-nav__link.is-current::after { left: var(--s-2); right: auto; width: 20px; bottom: 10px; }
  .js-ready .site-nav__cta { margin-top: var(--s-5); justify-content: center; }

  /* No-JS fallback: wrap the nav under the brand instead of overflowing. */
  html:not(.js-ready) .site-header__inner { flex-wrap: wrap; padding-block: var(--s-3); }
  html:not(.js-ready) .site-nav { flex-wrap: wrap; gap: var(--s-2); }
}


/* 6. SITE FOOTER =========================================================== */

.site-footer {
  background: var(--c-bg-sunk);
  border-top: var(--border);
  padding-block: var(--s-8) var(--s-5);
  margin-top: var(--section-y);
  color: var(--c-text-dim);
  font-size: var(--text-sm);
}

.site-footer__top {
  display: grid;
  gap: var(--s-7);
  grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr);
  padding-bottom: var(--s-7);
}

.site-footer__tagline {
  margin-top: var(--s-4);
  max-width: 34ch;
  color: var(--c-text-faint);
}

.site-footer__nav {
  display: grid;
  gap: var(--s-6);
  /* min(150px, 100%) rather than a bare 150px.
     A 1fr track carries an automatic minimum of min-content, and
     "info@apexcustomcoding.com" is a single unbreakable token, so the track grew
     past the viewport and every page could be swiped sideways on a phone. The
     min() caps the track's floor at the container width; overflow-wrap lets the
     address itself break rather than merely clip.

     Deliberately NOT fixed with overflow-x hidden on the body: that hides the
     symptom, and the next long string does the same thing invisibly. */
  grid-template-columns: repeat(auto-fit, minmax(min(150px, 100%), 1fr));
}

.site-footer__col { min-width: 0; }
.site-footer__list a { overflow-wrap: anywhere; }

.site-footer__heading {
  font-size: var(--text-xs);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--c-text-faint);
  margin-bottom: var(--s-3);
}

/* The footer links are on every page, and at 16px tall they were the smallest
   touch targets on the site: below the 24px WCAG 2.5.8 floor and easy to mis-tap
   into the wrong page from a phone. Padding grows the hit area without changing
   the type, and a coarse pointer gets the full 44px. The gap shrinks to absorb
   the padding, so the footer does not grow. */
.site-footer__list { display: grid; gap: 0; }
.site-footer__list a {
  display: block;
  padding-block: var(--s-2);
  color: var(--c-text-dim);
  text-decoration: none;
}
.site-footer__list a:hover { color: var(--c-text); text-decoration: underline; }
.site-footer__list a[aria-current="page"] { color: var(--c-text); }

.site-footer__bottom {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-3) var(--s-5);
  align-items: center;
  justify-content: space-between;
  padding-top: var(--s-5);
  border-top: 1px solid var(--c-line-soft);
  color: var(--c-text-faint);
  font-size: var(--text-xs);
}
@media (pointer: coarse) {
  .site-footer__list a { padding-block: 0.85rem; }
  .site-footer__legal a { display: inline-block; padding-block: 0.6rem; }
}

.site-footer__legal a { color: var(--c-text-faint); text-decoration: none; }
.site-footer__legal a:hover { color: var(--c-text-dim); text-decoration: underline; }

@media (max-width: 720px) {
  .site-footer__top { grid-template-columns: 1fr; gap: var(--s-6); }
}


/* 7. COMPONENTS ============================================================ */

/* -- Button ---------------------------------------------------------------- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-2);
  min-height: 46px;
  padding: var(--s-3) var(--s-5);
  border: 1px solid transparent;
  border-radius: var(--radius);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  line-height: 1.2;
  text-align: center;
  text-decoration: none;
  white-space: nowrap;
  transition: background-color var(--dur-fast) var(--ease),
              border-color var(--dur-fast) var(--ease),
              color var(--dur-fast) var(--ease),
              transform var(--dur-fast) var(--ease);
}
.btn:active { transform: translateY(1px); }

.btn--primary {
  background: var(--c-accent);
  color: var(--c-on-accent);
  box-shadow: var(--shadow-sm);
}
.btn--primary:hover { background: var(--c-accent-hot); color: var(--c-on-accent); }

.btn--secondary {
  background: var(--c-surface-2);
  border-color: var(--c-line);
  color: var(--c-text);
}
.btn--secondary:hover { background: var(--c-line-soft); border-color: var(--c-text-faint); color: var(--c-text); }

.btn--ghost { color: var(--c-text-dim); }
.btn--ghost:hover { color: var(--c-text); background: var(--c-surface-2); }

.btn--sm { min-height: 38px; padding: var(--s-2) var(--s-4); font-size: var(--text-sm); }
.btn--lg { min-height: 54px; padding: var(--s-4) var(--s-6); font-size: var(--text-md); }
.btn--block { display: flex; width: 100%; }

/* -- Eyebrow / label ------------------------------------------------------- */
.eyebrow {
  display: inline-block;
  font-size: var(--text-xs);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--c-accent);
}

/* -- Lede ------------------------------------------------------------------ */
.lede {
  max-width: var(--width-text);
  font-size: var(--text-md);
  color: var(--c-text-dim);
}

/* -- Badge ----------------------------------------------------------------- */
.badge {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  padding: var(--s-1) var(--s-3);
  border: 1px solid var(--c-line);
  border-radius: var(--radius-pill);
  background: var(--c-surface);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  color: var(--c-text-dim);
}
.badge--signal { border-color: color-mix(in srgb, var(--c-signal) 40%, transparent); color: var(--c-signal); }

/* -- Card ------------------------------------------------------------------ */
.card {
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
  padding: var(--s-5);
  background: var(--c-surface);
  border: var(--border);
  border-radius: var(--radius-lg);
  transition: border-color var(--dur) var(--ease), transform var(--dur) var(--ease-out);
}
.card__title { font-size: var(--text-md); }
.card__body { color: var(--c-text-dim); font-size: var(--text-sm); }

/* Only cards that are themselves a link should react to hover. */
a.card:hover, .card--interactive:hover {
  border-color: var(--c-accent-sunk);
  transform: translateY(-2px);
}
a.card { text-decoration: none; color: inherit; }

/* Case-study card: longer-form, with a heading that has to hold its own against a
   block of body copy rather than sit above one line. */
.card--study { gap: var(--s-4); }
.card--study .card__title { font-size: var(--text-lg); }
.card--study .card__label { color: var(--c-text); font-weight: var(--weight-bold); }

/* -- Numbered step (How It Works is built from these) ----------------------- */
.step { display: grid; grid-template-columns: auto 1fr; gap: var(--s-4); align-items: start; }
.step__num {
  display: grid;
  place-items: center;
  width: 2.25rem; height: 2.25rem;
  border-radius: 50%;
  border: 1px solid var(--c-accent-sunk);
  background: var(--c-surface);
  color: var(--c-accent);
  font-size: var(--text-sm);
  font-weight: var(--weight-bold);
  font-variant-numeric: tabular-nums;
}
.step__body { color: var(--c-text-dim); }
.step__title { color: var(--c-text); margin-bottom: var(--s-2); }

/* -- Rule / divider -------------------------------------------------------- */
.rule { height: 1px; background: var(--c-line); border: 0; }


/* 8. UTILITIES ============================================================= */
/* Deliberately few. If you need a fifth utility, you probably need a component. */

.u-visually-hidden {
  position: absolute !important;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.u-center { margin-inline: auto; text-align: center; }
.u-dim { color: var(--c-text-dim); }
.u-faint { color: var(--c-text-faint); }
.u-nowrap { white-space: nowrap; }


/* 9. MOTION + PRINT ======================================================== */

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

@media print {
  .site-header, .site-footer, .skip-link, .prelaunch { display: none !important; }
  body { background: #fff; color: #000; }
  a[href^="http"]::after { content: " (" attr(href) ")"; font-size: 0.8em; }
}


/* 10. HERO ================================================================
   Layout and type live on .hero and apply in BOTH states, so switching
   HERO_VIDEO off cannot leave a hole where the video was, the static hero is
   the same size, the same copy and the same layout, just without the layer.
   .hero--video adds the media and the scrim and nothing else.

   Scrim is a gradient, not a wash: heavy behind the text, clearing to almost
   nothing at the right so the footage is actually visible. The floor across the
   text band is 0.80, measured, see the contrast note below. */

.hero {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  display: flex;
  align-items: center;
  /* Deliberately not 100vh: that ignores mobile browser chrome and pushes the
     CTA under the fold on exactly the phones that matter.
     Brought in from 76svh/42rem, at that size the hero was taller than its own
     content and left a dead band under the CTAs. */
  min-height: min(66svh, 36rem);
  padding-block: var(--s-7);
}

/* The prominent placement. Sized against viewport HEIGHT as well as width:
   width-only clamps are exactly what breaks landscape phone. */
.hero__mark {
  display: block;
  width: clamp(4rem, min(8.5vw, 13svh), 8.75rem);
  height: auto;
  color: #fff;
  margin-bottom: clamp(0.5rem, 1.4vh, 1.25rem);
}

/* Guardrail: where height is tight the mark goes, not the message. The header
   lockup still carries the brand. The h1, sub and CTA never move for the logo. */
@media (max-height: 600px) {
  .hero__mark { display: none; }
}

/* Keeps the headline out of the cleared right-hand side of the scrim, which is
   what makes the measured contrast floor hold at every viewport width. */
.hero__content { position: relative; z-index: 1; width: 100%; }
.hero__content > * { max-width: none; }

/* Ceiling raised from 4.2rem, it used to stop growing around 1300px, so on a
   large monitor the headline was undersized. 4.75rem is the largest that still
   sets on two lines inside the scrim's dark band; past that the third line
   returns or the headline runs into the cleared area where the video shows. */
.hero h1 {
  font-size: clamp(2.5rem, 1.9rem + 2.8vw, 4.75rem);
  max-width: min(100%, 62rem);
}

.hero .lede {
  color: var(--c-text);
  font-size: clamp(1.12rem, 1.03rem + .38vw, 1.4rem);
  /* Reading measure, deliberately narrower than the headline. */
  max-width: min(100%, 40rem);
}

.hero--video .hero__media {
  position: absolute;
  inset: 0;
  z-index: -3;
  background-color: var(--c-bg);
  background-image: url("/assets/media/hero-poster-960.webp");
  background-size: cover;
  background-position: center;
}

@media (min-width: 961px) {
  .hero--video .hero__media { background-image: url("/assets/media/hero-poster.webp"); }
}

.hero__video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;                       /* revealed only once it is genuinely playing */
  transition: opacity var(--dur-slow) var(--ease);
}
.hero__video.is-playing { opacity: 1; }

/* The loop's brightest pixel behind the headline is rgb(200,246,236), 0.99:1
   against --c-text unscrimmed. At 0.80 scrim alpha that becomes 9.4:1. The stops
   from 0% to 56% are the text band and must never drop below .80; past that the
   scrim is free to clear. Re-measure the footage before touching these. */
.hero__scrim {
  position: absolute;
  inset: 0;
  z-index: -1;
  /* Narrow viewports: the 40rem text block occupies most of the width, so the
     scrim has to stay heavy nearly to the right edge. Still a gradient, but a
     shallow one, there is simply less room to clear. */
  background: linear-gradient(90deg,
    rgba(11, 13, 17, .94) 0%,
    rgba(11, 13, 17, .88) 60%,
    rgba(11, 13, 17, .80) 90%,
    rgba(11, 13, 17, .72) 100%);
}

/* The container only stops growing once the viewport exceeds its 1440px cap plus
   both gutters, 1520px, not 1200px. Below that the text still reaches into the
   right-hand side, so the heavier scrim above has to stay. Switching at 1200px
   dropped the worst case to 6.0:1; switching here keeps it above 10:1. */
@media (min-width: 1520px) {
  .hero__scrim {
    background: linear-gradient(90deg,
      rgba(11, 13, 17, .94) 0%,
      rgba(11, 13, 17, .90) 45%,
      rgba(11, 13, 17, .80) 68%,
      rgba(11, 13, 17, .42) 84%,
      rgba(11, 13, 17, .16) 100%);
  }
}

/* Landscape phone, roughly 375px of height. Content fitting beats a full-bleed
   hero: the CTA has to stay above the fold here or the hero has failed. */
@media (max-height: 560px) and (orientation: landscape) {
  .hero { min-height: 0; padding-block: var(--s-3); }
  .hero h1 { font-size: clamp(1.6rem, 1.25rem + 1.5vw, 2rem); }
  .hero .lede { font-size: var(--text-base); }
  .hero .stack--loose { --flow: var(--s-3); }
  .hero .stack { --flow: var(--s-3); }
  /* Large buttons cost 54px of a 375px screen for no benefit at this size. */
  .hero .btn--lg { min-height: 44px; padding: var(--s-2) var(--s-5); font-size: var(--text-sm); }
}

@media (prefers-reduced-motion: reduce) {
  .hero__video { display: none !important; }
}

/* Below 768px the video is never requested; the poster carries the hero alone. */
@media (max-width: 767px) {
  .hero__video { display: none; }
}

/* Narrow phones. The headline is the biggest consumer of vertical space here, so
   it comes down first, the CTA staying above the fold matters more than type scale. */
@media (max-width: 560px) {
  /* No forced height on a phone. min-height was overshooting the content and
     leaving a visible void between the CTAs and the next section. */
  .hero { padding-block: var(--s-6); min-height: 0; }

  /* Shrink-to-fit buttons stack at two different widths, which reads as unfinished.
     Full width is the standard phone pattern and gives a bigger tap target. */
  .hero .cluster { flex-direction: column; align-items: stretch; }
  .hero .cluster .btn { width: 100%; }
  .hero h1 { font-size: clamp(1.9rem, 1.55rem + 2.2vw, 2.5rem); }
  .hero .lede { font-size: 1.02rem; }
  .hero .stack--loose { --flow: var(--s-5); }
  .hero .btn--lg { min-height: 48px; }
}


/* 11. FORMS ================================================================ */
/* Only the contact form uses these today, but they are written as a component
   because the next form will otherwise reinvent them badly. */

.field { display: grid; gap: var(--s-2); }

.field__label {
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  color: var(--c-text);
}
.field__req,
.field__opt {
  font-weight: var(--weight-normal);
  color: var(--c-text-faint);
}

.field__input {
  width: 100%;
  padding: var(--s-3) var(--s-4);
  background: var(--c-surface-2);
  border: 1px solid var(--c-line);
  border-radius: var(--radius);
  color: var(--c-text);
  font-size: var(--text-base);   /* 16px minimum: anything smaller makes iOS zoom on focus */
  line-height: 1.5;
  transition: border-color var(--dur-fast) var(--ease), background-color var(--dur-fast) var(--ease);
}
.field__input::placeholder { color: var(--c-text-faint); }
.field__input:hover { border-color: var(--c-text-faint); }
.field__input:focus { background: var(--c-surface); }

.field__input--area { resize: vertical; min-height: 11rem; }

/* Error state is never colour alone, there is a message, a border change and an
   aria-describedby link, so it survives colour blindness and a screen reader. */
.field__input[aria-invalid="true"] { border-color: var(--c-error); }

.field__error {
  font-size: var(--text-sm);
  color: var(--c-error);
}

.form__summary {
  margin-bottom: var(--s-5);
  padding: var(--s-4);
  border: 1px solid var(--c-error);
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--c-error) 10%, transparent);
  color: var(--c-text);
  font-size: var(--text-sm);
}

.form.stack { --flow: var(--s-5); }


/* 12. STYLEGUIDE ONLY ======================================================
   The internal styleguide is a document about literal values: token names, class
   names, code samples. A monospace face is the signal that separates those from
   prose, so the page keeps it. The page is gated on SITE_PRELAUNCH and 404s at
   launch, so no visitor can reach any of this.

   Every selector here is scoped to .page--styleguide, including the token itself.
   Nothing in this block is reachable from any other page. Do not lift any of it
   into :root. If a customer-facing page ever genuinely needs to show a file path
   or a snippet, that is a decision for the admin, not a CSS change. */

.page--styleguide {
  --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
               "Liberation Mono", monospace;
}

.page--styleguide code,
.page--styleguide kbd,
.page--styleguide samp,
.page--styleguide pre {
  font-family: var(--font-mono);
  font-size: 0.92em;
}

/* Supporting line under a hero headline. Smaller than the h1 and larger than the
   lede, so the pair reads as problem then answer rather than as two headlines. */
.hero__sub {
  font-size: clamp(1.35rem, 1.1rem + 1.1vw, 2rem);
  font-weight: var(--weight-medium);
  color: var(--c-accent-hot);
  letter-spacing: var(--tracking-tight);
  max-width: min(100%, 46rem);
}

/* Guidance under a field. Distinct from an error: quieter, always present, and it
   sits between the control and any error so the two never read as one message. */
.field__help {
  font-size: var(--text-sm);
  color: var(--c-text-faint);
}

.field__input--short { min-height: 6rem; }

/* Quoted statements. Used where the copy is things a reader has said out loud, so
   they should read as voices rather than as a feature list. */
.quote-list { display: grid; gap: var(--s-3); }
.quote-list li {
  padding: var(--s-3) var(--s-4);
  border-left: 2px solid var(--c-accent-sunk);
  background: var(--c-surface);
  border-radius: 0 var(--radius) var(--radius) 0;
  color: var(--c-text);
}

/* Choice groups: checkboxes and radios for the process assessment (TASK-0059).
   Deliberately minimal. No new colours, no new radii, no new spacing values, and
   the native control is left visible rather than replaced with a drawn one: a
   custom checkbox is a new component to keep accessible forever, and there was no
   reason to take that on for one page. */
.choices {
  display: grid;
  gap: var(--s-2);
  margin: 0;
  padding: 0;
  border: 0;
}

@media (min-width: 40rem) {
  .choices--two { grid-template-columns: 1fr 1fr; }
}

.choice {
  display: flex;
  gap: var(--s-3);
  align-items: baseline;
  padding: var(--s-2) var(--s-3);
  border: 1px solid var(--c-line);
  border-radius: var(--radius);
  background: var(--c-surface-2);
  cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease), background-color var(--dur-fast) var(--ease);
}

.choice:hover { border-color: var(--c-text-faint); }

/* The whole row reacts to keyboard focus, not just the 16px control inside it. */
.choice:focus-within {
  border-color: var(--c-text);
  background: var(--c-surface);
}

.choice input {
  flex: none;
  width: 1.05rem;
  height: 1.05rem;
  margin: 0;
  accent-color: var(--c-accent);
}

.choice__label {
  font-size: var(--text-base);
  line-height: 1.4;
}

/* ---------------------------------------------------------------------------
   Guided assessment (/find-the-process).

   The whole page is one <form>. Scripting splits it into panels; without
   scripting every panel is visible and it submits in one go, which is the same
   thing the page did before the redesign. Nothing here is required for the form
   to work, only for it to feel like a conversation instead of a document.
--------------------------------------------------------------------------- */

.ask__intro-inner { max-width: 44rem; }

.ask__title {
  font-size: clamp(2.2rem, 6vw, 3.4rem);
  line-height: 1.05;
  letter-spacing: -0.02em;
  margin-bottom: var(--s-5);
}

.ask__lede {
  font-size: clamp(1.15rem, 2.4vw, 1.5rem);
  line-height: 1.4;
  color: var(--c-text);
  margin-bottom: var(--s-5);
}

.ask__body { max-width: 40rem; }

/* Three value points. A list, not cards: cards would add borders and shadows to
   a page whose whole job is to feel unhurried. */
.ask__points {
  list-style: none;
  margin: var(--s-9) 0 var(--s-8);
  padding: 0;
  display: grid;
  gap: var(--s-6);
}

@media (min-width: 52rem) {
  .ask__points { grid-template-columns: repeat(3, 1fr); gap: var(--s-7); }
}

.ask__points h2 {
  font-size: var(--text-base);
  letter-spacing: 0.02em;
  margin-bottom: var(--s-2);
}

.ask__points p { color: var(--c-text-muted); margin: 0; }

.ask__start { margin-top: var(--s-7); }

/* --- the questions ------------------------------------------------------- */

.ask .container { max-width: 44rem; }

.ask__steps {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2) var(--s-5);
  margin: 0 0 var(--s-8);
  padding: 0;
}

.ask__step {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  font-size: var(--text-sm);
  color: var(--c-text-faint);
}

.ask__step-num {
  display: inline-grid;
  place-items: center;
  width: 1.6rem;
  height: 1.6rem;
  border: 1px solid var(--c-line);
  border-radius: var(--radius-pill);
  font-size: 0.8rem;
}

.ask__step.is-current { color: var(--c-text); }
.ask__step.is-current .ask__step-num { border-color: var(--c-accent); color: var(--c-accent); }
.ask__step.is-done { color: var(--c-text-muted); }
.ask__step.is-done .ask__step-num { border-color: var(--c-text-muted); }

/* One question at a time, spacious. */
.ask__panel + .ask__panel { margin-top: var(--s-10); }

.ask__q {
  font-size: clamp(1.35rem, 3.2vw, 1.9rem);
  line-height: 1.25;
  letter-spacing: -0.01em;
  margin: 0 0 var(--s-3);
}

.ask__q--sub { font-size: clamp(1.05rem, 2.2vw, 1.25rem); }

.ask__help {
  color: var(--c-text-muted);
  margin: 0 0 var(--s-5);
  max-width: 38rem;
}

.ask__input { font-size: var(--text-base); }
.ask__input[rows] { min-height: 8rem; resize: vertical; }

/* One line high, and that is the point. The height of the box is the instruction:
   the tall box on the step before it was asking a stranger for an essay before
   they had agreed to anything. */
.ask__input--short { min-height: 0; }

/* What they picked on step 1, quoted back. Set apart from the help text under it
   so it reads as their words rather than as more instructions. */
.ask__context {
  border-left: 2px solid var(--c-accent);
  padding-left: 0.75rem;
  color: var(--c-text-dim);
}

.ask__examples {
  margin-top: var(--s-4);
  font-size: var(--text-sm);
  color: var(--c-text-faint);
  line-height: 1.6;
}

.ask__sub { margin-top: var(--s-8); }

.ask__grid { display: grid; gap: var(--s-5); }

@media (min-width: 40rem) {
  .ask__grid { grid-template-columns: 1fr 1fr; }
}

.ask__nav {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-4);
  align-items: center;
  margin-top: var(--s-9);
  padding-top: var(--s-7);
  border-top: 1px solid var(--c-line);
}

.ask__nav .ask__next, .ask__nav .ask__submit { margin-left: auto; }

/* --- scripted behaviour -------------------------------------------------- */

/* Only ever hides anything when scripting is present. Without it, every panel
   stays visible and the form behaves exactly as it did before. */
.js-ready .ask__panel[hidden] { display: none; }
.js-ready .ask__steps[aria-hidden="true"] { display: none; }

/* --- confirmation -------------------------------------------------------- */

.ask__done { max-width: 40rem; }
.ask__done-title { font-size: clamp(2rem, 5vw, 2.8rem); margin-bottom: var(--s-5); }
.ask__done-lede { font-size: clamp(1.05rem, 2.2vw, 1.25rem); margin-bottom: var(--s-5); }
.ask__done-line { margin-top: var(--s-5); }

@media (prefers-reduced-motion: no-preference) {
  .ask__panel { animation: ask-in var(--dur-fast) var(--ease) both; }
  @keyframes ask-in { from { opacity: 0; transform: translateY(0.4rem); } to { opacity: 1; transform: none; } }
}

/* The hidden attribute must win over any component's display.
   .btn sets display:inline-flex, which beats the user-agent [hidden] rule, so
   buttons the script had "hidden" stayed on screen. Found by a real browser at
   four viewport sizes; no amount of reading the markup would have shown it. */
[hidden] { display: none !important; }

/* A button must never be wider than what contains it.
   .btn sets white-space: nowrap, which is right almost everywhere and wrong on a
   320px screen, where "Tell Us What's Not Working" pushed 9px past the viewport
   and made every page swipeable sideways. The label is allowed to wrap only on
   screens narrow enough to need it, so nothing changes on ordinary phones. */
.btn { max-width: 100%; }

@media (max-width: 24rem) {
  .btn { white-space: normal; }
}

/* --- Assessment: state, progress and balance (TASK-0064) ------------------ */

/* The SHELL is hidden before Start, not its contents.
   Hiding only the panels still paid for the section's padding and container:
   595px of empty on desktop, 373px on a phone. Scoped to .js because with
   scripting off nothing would ever reveal it. */
.js-ready .ask.is-idle { display: none; }

/* Panels are NOT marked hidden in the markup, deliberately. The global
   [hidden] rule is not scoped to .js, so a hidden attribute there would hide
   them for a no-scripting visitor too and leave nothing to reveal them. Before
   Start the shell above is what hides them; after Start the script sets the
   attribute on the five that are not current. */
.js-ready .ask__panel:not(:first-of-type) { display: none; }
.js-ready .ask.is-started .ask__panel { display: block; }
.js-ready .ask.is-started .ask__panel[hidden] { display: none; }

/* A tall stage followed by a short one must not leave the short one padded out
   to the tall one's height. Nothing here sets a min-height, and this asserts it
   stays that way. */
.ask__form { min-height: 0; }

/* Balance the hero. It was a 44rem column pinned to the left of a 1440px
   viewport with the right half empty, which reads as unfinished rather than
   spacious. The measure stays comfortable for reading; the block is centred and
   the three points use the full width. */
.ask__intro-inner { max-width: 52rem; margin-inline: auto; }
.ask__intro .ask__body { max-width: 44rem; }
.ask__intro { padding-block: var(--s-9); }

/* --- progress ------------------------------------------------------------ */

.ask__progress { margin-bottom: var(--s-8); }

.ask__steps {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2) var(--s-5);
  margin: 0;
  padding: 0;
}

/* Six labels do not fit on a phone, so below this width the compact form takes
   over: STEP n OF 6, the stage name, and a bar. */
@media (max-width: 47rem) {
  .ask__steps { display: none; }
}

@media (min-width: 47.01rem) {
  .ask__progress-compact, .ask__bar { display: none; }
}

.ask__progress-compact {
  display: flex;
  align-items: baseline;
  gap: var(--s-3);
  margin: 0 0 var(--s-3);
}

.ask__progress-count {
  font-size: var(--text-xs);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--c-text-faint);
}

.ask__progress-name { font-size: var(--text-sm); color: var(--c-text); }

.ask__bar {
  height: 2px;
  background: var(--c-line);
  border-radius: var(--radius-pill);
  overflow: hidden;
}

.ask__bar-fill {
  display: block;
  height: 100%;
  width: 0;
  background: var(--c-accent);
  transition: width var(--dur) var(--ease);
}

@media (prefers-reduced-motion: reduce) {
  .ask__bar-fill { transition: none; }
}

/* --- footer on narrow screens -------------------------------------------- */

/* One column below this width so the email address fits on a line. It was
   breaking mid-word, reading "info@apexcustomcodi / ng.com", because the wrap
   rule was doing work the layout should have done. */
@media (max-width: 30rem) {
  .site-footer__nav { grid-template-columns: 1fr; }
  .site-footer__list a { overflow-wrap: normal; }
}

/* Without scripting the navigation stays a plain visible list, because nothing
   would ever open a drawer. On a narrow screen six items do not fit on one line,
   so it must wrap. It overflowed by 94px at 390px once the list gained a sixth
   entry, and only in the no-scripting path, which is exactly the path nothing
   was watching. */
html:not(.js-ready) .site-nav__list {
  flex-wrap: wrap;
  justify-content: center;
}

@media (max-width: 47rem) {
  html:not(.js-ready) .site-nav { flex-wrap: wrap; justify-content: center; gap: var(--s-3); }
  html:not(.js-ready) .site-nav__cta { margin-left: 0; }
}

/* The size-of-the-ask line under Start. Quiet, and never a second call to action. */
.ask__reassure {
  margin-top: var(--s-3);
  font-size: var(--text-sm);
  color: var(--c-text-muted);
}

.ask__proof {
  margin-top: var(--s-2);
  font-size: var(--text-sm);
  color: var(--c-text-muted);
}

/* Exit ramp. Sits outside the panels so it is present at every stage, and below
   the controls so it never competes with Continue. */
.ask__exit {
  margin-top: var(--s-7);
  font-size: var(--text-sm);
  color: var(--c-text-faint);
}

.ask__exit a { color: var(--c-text-muted); }
.ask__exit a:hover { color: var(--c-text); }

/* Use the width at 1440. The hero was a centred column with the right half of the
   most valuable space on the page empty, which reads as unfinished rather than
   confident. Nothing invented to fill it: the copy and the three points simply
   stop being squeezed into half the viewport. */
@media (min-width: 68rem) {
  .ask__intro-inner {
    max-width: none;
    display: grid;
    grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr);
    column-gap: var(--s-10);
    align-items: start;
  }

  .ask__intro-lead { grid-column: 1; }
  .ask__intro .ask__points { grid-column: 2; grid-row: 1 / span 2; margin-top: 0; }
  .ask__intro .ask__start, .ask__intro .ask__reassure { grid-column: 1; }
  .ask__intro .ask__points { grid-template-columns: 1fr; gap: var(--s-6); }
}

/* --- Chips (TASK-0067) ----------------------------------------------------
   A rounded pill per option, wrapping into a soft block rather than a column of
   boxes. Twelve options then read as a glance, which is the point of moving that
   question to the front.

   Real inputs underneath. The input is visually hidden but still focusable and
   still announced, and every selected state comes from :checked in CSS, so this
   works with scripting off and with a script that never arrives.
--------------------------------------------------------------------------- */

/* Grouping the choices in a fieldset is the accessible markup, but no rule ever
   reset one, so every question group was drawing the user agent's 2px groove
   border with the legend punched through it. Nothing in the design system has a
   groove border. The group is identified by its legend, not by a box. */
fieldset {
  border: 0;
  margin: 0;
  padding: 0;
  min-width: 0; /* fieldsets refuse to shrink below their content without this */
}

legend {
  padding: 0;
}

.chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
  margin: 0;
  padding: 0;
  border: 0;
}

.chip {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  min-height: 44px;
  padding: var(--s-2) var(--s-4);
  border: 1px solid var(--c-line);
  border-radius: var(--radius-pill);
  background: var(--c-surface-2);
  color: var(--c-text-muted);
  cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease),
              background-color var(--dur-fast) var(--ease),
              color var(--dur-fast) var(--ease);
}

.chip:hover { border-color: var(--c-text-faint); color: var(--c-text); }

/* Sentence-length options. One column where there is no room for two, two from
   the point where a line of body text gets uncomfortably wide. Equal-height rows
   come free with grid, so no card sits proud of its neighbour. */
.chips--cards {
  display: grid;
  grid-template-columns: 1fr;
  align-items: stretch;
}

@media (min-width: 46em) {
  .chips--cards { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

.chips--cards .chip {
  align-items: flex-start;
  border-radius: var(--radius);
  padding: var(--s-3) var(--s-4);
}

/* The mark stays on the first line of a two-line option rather than centring
   itself against the whole block, so a wrapped option still reads as one row. */
.chips--cards .chip__mark { margin-top: 1px; }

/* Hidden from sight, not from the keyboard or from assistive technology. */
.chip__input {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: 0;
  padding: 0;
  border: 0;
  opacity: 0;
  pointer-events: none;
}

/* The whole chip shows focus, because the input itself is 1px. */
.chip:focus-within {
  outline: 2px solid var(--c-accent);
  outline-offset: 2px;
}

/* Selected. Never colour alone: the border thickens and a check mark appears. */
.chip:has(.chip__input:checked) {
  background: var(--c-surface);
  border-color: var(--c-accent);
  box-shadow: inset 0 0 0 1px var(--c-accent);
  color: var(--c-text);
}

.chip__mark {
  /* Decorative. Without this it sits on top of the label and swallows the click
     at its own coordinates, which a real browser recovers from through label
     delegation and an automated click does not. Either way it should never be
     the thing a pointer lands on. */
  pointer-events: none;
  display: inline-grid;
  place-items: center;
  width: 1rem;
  height: 1rem;
  flex: none;
  border-radius: var(--radius-pill);
  border: 1px solid var(--c-line);
  transition: border-color var(--dur-fast) var(--ease), background-color var(--dur-fast) var(--ease);
}

.chip:has(.chip__input:checked) .chip__mark {
  background: var(--c-accent);
  border-color: var(--c-accent);
}

.chip:has(.chip__input:checked) .chip__mark::after {
  content: "";
  width: 0.32rem;
  height: 0.6rem;
  border: solid var(--c-bg);
  border-width: 0 2px 2px 0;
  transform: translateY(-1px) rotate(45deg);
}

/* Radios read as a single choice, so their marker is a dot rather than a tick. */
.chip:has(.chip__input[type="radio"]:checked) .chip__mark::after {
  width: 0.4rem;
  height: 0.4rem;
  border: 0;
  border-radius: 50%;
  background: var(--c-bg);
  transform: none;
}

.chip__label { font-size: var(--text-sm); line-height: 1.3; pointer-events: none; }

/* :has() is widely supported now, but a browser without it must still show the
   selection. The input is only visually hidden, so this restores a visible
   native control rather than leaving a chip that never looks chosen. */
@supports not selector(:has(*)) {
  .chip__input {
    position: static;
    width: 1.05rem;
    height: 1.05rem;
    opacity: 1;
    pointer-events: auto;
    accent-color: var(--c-accent);
  }
  .chip__mark { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .chip, .chip__mark { transition: none; }
}


/* --- Work page product screenshots -----------------------------------------
   Three faults this replaces, all measured on the live page rather than guessed:
   a two-column auto-fit grid became FOUR columns at 1440, so 1280px captures
   rendered 323px wide and nothing in them was legible; a 375x812 phone capture in
   a cell sized by width became 698px tall beside 203px neighbours; and EnviroLog,
   being alone in its row, filled 1280px while every Make Safety Easy shot was
   squeezed to 323. The last one is the important one: how big a screenshot appears
   must not depend on how many screenshots its product happens to have.

   So: a fixed two-column grid, and every tile the same shape. The image is
   contained inside the tile and never scaled up, which makes a portrait phone
   capture read as a phone standing next to the desktop views rather than as the
   largest object on the page.
--------------------------------------------------------------------------- */

/* Standalone on its own line rather than inline in a sentence, so the 24px floor
   applies to it with no exception to hide behind. Padding only, the type is
   unchanged. The one link left under 24px on the site is "See the work", which IS
   inline in a sentence: WCAG 2.5.8 excepts those, and padding a mid-sentence link
   breaks the line spacing around it. */
.study__onward {
  margin-top: var(--s-5);
  color: var(--c-text-dim);
}

.study__host {
  display: inline-block;
  padding-block: var(--s-1);
}

.study__shots {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--s-5);
  margin: 0;
  padding: 0;
  list-style: none;
}

@media (min-width: 46em) {
  /* Fixed at two. auto-fit is what produced four columns of unreadable thumbnails. */
  .study__shots { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

.study__shot { margin: 0; min-width: 0; }
.study__fig  { margin: 0; }

.study__zoom {
  /* The tile. Same shape for every shot, so scale never depends on the image. */
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
  aspect-ratio: 16 / 10;
  overflow: hidden;
  padding: var(--s-2);
  border: 1px solid var(--c-line);
  border-radius: var(--radius);
  background: var(--c-bg-sunk);
  transition: border-color var(--dur-fast) var(--ease);
}

.study__zoom:hover,
.study__zoom:focus-visible { border-color: var(--c-accent); }

/* <picture> is the flex item, not the <img> inside it, so the image's
   max-height resolved against a parent with no definite height and a portrait
   capture simply overflowed the tile. display:contents makes the img the flex
   item itself, so it is constrained by the tile it sits in. */
.study__zoom picture { display: contents; }

.study__img {
  display: block;
  min-height: 0;
  min-width: 0;
  /* auto plus a max is what stops any capture being displayed above its own
     pixels. width:100% would upscale the 375px phone shot to fill the tile. */
  width: auto;
  height: auto;
  max-width: 100%;
  max-height: 100%;
  border-radius: calc(var(--radius) - 4px);
}

.study__zoom-hint {
  position: absolute;
  right: var(--s-2);
  bottom: var(--s-2);
  padding: 2px var(--s-2);
  border-radius: var(--radius-sm);
  background: rgb(7 9 12 / 0.82);
  color: var(--c-text);
  font-size: var(--text-xs);
  opacity: 0;
  transition: opacity var(--dur-fast) var(--ease);
}

.study__zoom:hover .study__zoom-hint,
.study__zoom:focus-visible .study__zoom-hint { opacity: 1; }

.study__caption {
  margin-top: var(--s-2);
  font-size: var(--text-sm);
  color: var(--c-text-faint);
}

/* --- Enlarged view ---------------------------------------------------------
   A native dialog, so Escape, the focus trap and returning focus to the link
   that opened it are the browser's job rather than ours. The plain link is the
   fallback and it works on its own: with no script, clicking opens the image.
--------------------------------------------------------------------------- */

.lightbox {
  max-width: min(96vw, 1400px);
  max-height: 94vh;
  padding: 0;
  border: 1px solid var(--c-line);
  border-radius: var(--radius);
  background: var(--c-bg-sunk);
  color: var(--c-text);
}

.lightbox::backdrop { background: rgb(3 4 6 / 0.86); }

.lightbox__img {
  display: block;
  max-width: 100%;
  max-height: calc(94vh - 3.5rem);
  width: auto;
  height: auto;
  margin: 0 auto;
}

.lightbox__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-4);
  padding: var(--s-2) var(--s-3);
  border-top: 1px solid var(--c-line);
  font-size: var(--text-sm);
  color: var(--c-text-dim);
}

.lightbox__close {
  flex: none;
  padding: var(--s-1) var(--s-3);
  border: 1px solid var(--c-line);
  border-radius: var(--radius-sm);
  background: var(--c-surface);
  color: var(--c-text);
  font: inherit;
  cursor: pointer;
}

.lightbox__close:hover { border-color: var(--c-text-faint); }



/* The privacy link beside a submit control. Quiet, close to the button, and never
   a checkbox: consent adds nothing here and a gate would only cost submissions. */
.form__privacy {
  margin-top: var(--s-3);
  font-size: var(--text-sm);
}


/* The worked example. Set apart from the prose so the arithmetic reads as an
   illustration rather than as a claim, and left-aligned so a reader can follow the
   multiplication line by line. */
.cost-sum {
  padding: var(--s-4);
  border-left: 2px solid var(--c-accent);
  background: var(--c-surface);
  border-radius: var(--radius-sm);
  line-height: 1.9;
}
