/* ==========================================================================
   Elijah Short Web Design
   Shared stylesheet. Same pattern as the Migrant Connections Taranaki build:
   every token in :root, mobile-first, system fonts so there are zero
   third-party requests.

   Palette sampled from the logo (1x/ESWD.png):
     charcoal #2A3137, cyan #0FC4E0, teal-green #37C09B.
   The logo cyan is far too light for white text (2.1:1), so it is darkened
   to #097586 for buttons and links, which is 5.38:1 against white. Same
   move as the MCT orange being darkened to #C25423.

   Change a token here and it changes everywhere. When you edit this file,
   bump the ?v= string in every HTML file. See .htaccess.
   ========================================================================== */

/* ==========================================================================
   TYPEFACES — added 28 Aug 2026.

   The build shipped on system fonts and read as plain. Two faces now do the
   work, both SIL Open Font Licence, both SELF-HOSTED from /assets/fonts.
   Nothing is fetched from Google Fonts or any other origin: the licence text
   sits beside the files, and the whole point of self-hosting is that the
   "no third-party requests" claim on the home page stays true.

   THE HOME PAGE STAT CHANGED BECAUSE OF THIS. It used to read "0 trackers,
   web fonts or third-party requests". There are now web fonts, so the line
   reads "0 trackers or third-party requests", which is still true and still
   checkable. Do not put the old wording back.

     DM Sans Bold      12.3 KB   headings, brand, buttons, big numerals
     IBM Plex Mono Bd   7.4 KB   eyebrows, labels, meta, captions

   19.7 KB total, both subset with pyftsubset to Latin plus the punctuation
   this site actually uses, both preloaded in every <head>. Body copy stays on
   system fonts: it renders on the first paint with no swap, and it is the
   majority of the text on every page.

   font-display: swap, so a slow font never blocks text. The fallback stacks
   are size-adjusted below so the swap moves the line as little as possible.
   ========================================================================== */
@font-face {
  font-family: "DM Sans";
  src: url("../fonts/dmsans-bold.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "IBM Plex Mono";
  src: url("../fonts/plexmono-bold.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}
/* Metric-matched fallbacks. Without these the swap from the system font to
   DM Sans shifts every heading and shows up as CLS. Ratios measured against
   Arial/Helvetica, which is what the fallback resolves to on the platforms
   that matter here. */
@font-face {
  font-family: "DM Sans Fallback";
  src: local("Arial Bold"), local("Helvetica Bold"), local("Arial");
  font-weight: 700;
  size-adjust: 101%;
  ascent-override: 96%;
  descent-override: 24%;
  line-gap-override: 0%;
}

:root {
  /* Brand, sampled from the logo. The two bright ones are for use on dark
     backgrounds only, never behind white text. */
  --cyan:        #0FC4E0;   /* 7.75:1 on --ink-deep */
  --cyan-pale:   #8FE3EE;   /* eyebrows on the tinted hero. See note there. */
  --teal:        #37C09B;   /* 7.12:1 on --ink-deep */
  --accent:      #097586;   /* 5.38:1 with white. Buttons, links, eyebrows. */
  --accent-dark: #07606E;   /* 7.22:1 with white. Hover. */
  --accent-pale: #EDF6F8;

  /* Neutrals */
  --ink-deep:  #0C232A;   /* headings, dark sections. 16.3:1 on white */
  --ink:       #2A3137;   /* body. 13.2:1 on white */
  --ink-soft:  #525E69;   /* secondary. 6.6:1 on white */
  --line:      #E1E7EA;
  --bg:        #FFFFFF;
  --bg-tint:   #F4F7F8;

  /* On dark backgrounds */
  /* On dark backgrounds. Both are checked against the LIGHTEST point of the
     hero gradient (#135256), not against the flat --ink-deep, because that
     is where they actually have to survive. */
  --on-dark:      #E6EDEF;   /* 13.8:1 flat, 7.5:1 at the gradient's lightest */
  --on-dark-soft: #C3D2D6;   /* 10.5:1 flat, 5.7:1 at the gradient's lightest */

  /* Type scale. clamp() so it scales without breakpoint jumps. */
  --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
          "Helvetica Neue", Arial, sans-serif;
  /* Headings, brand, buttons and the big numerals. */
  --font-display: "DM Sans", "DM Sans Fallback", var(--font);
  /* Eyebrows, meta lines, captions, footer headings. Uppercase and tracked
     out, never used for anything longer than about six words. */
  --font-mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas,
               "Liberation Mono", monospace;
  --step--2: clamp(0.83rem, 0.81rem + 0.1vw, 0.89rem);
  --step--1: clamp(0.94rem, 0.92rem + 0.1vw, 1rem);
  --step-0:  clamp(1.06rem, 1rem + 0.25vw, 1.19rem);
  --step-1:  clamp(1.3rem, 1.2rem + 0.5vw, 1.6rem);
  --step-2:  clamp(1.6rem, 1.4rem + 1vw, 2.3rem);
  --step-3:  clamp(2rem, 1.6rem + 2vw, 3.2rem);
  /* The top of the scale was 3.6rem and the h1 never felt like the largest
     thing on the page. In DM Sans Bold at -0.035em it carries 4.4rem without
     crowding, and the hero headline is the one place the site gets to be
     loud. The lower bound is unchanged, so nothing moves on a phone. */
  --step-4:  clamp(2.3rem, 1.6rem + 3.4vw, 4.4rem);

  /* Spacing */
  --sp-xs:  0.5rem;
  --sp-s:   1rem;
  --sp-m:   1.75rem;
  --sp-l:   3rem;
  --sp-xl:  4.5rem;
  --sp-2xl: 6.5rem;

  --wrap: 1140px;
  --wrap-narrow: 740px;
  --radius: 10px;
  --shadow: 0 2px 4px rgba(12,35,42,.05), 0 10px 30px rgba(12,35,42,.07);

  /* THE 45 -------------------------------------------------------------
     The logo mark is a maunga: an isoceles triangle whose faces run at
     almost exactly 45 degrees. Measured off assets/img/eswd-mark.svg, apex
     (120,4) to base (236,118) is dx 116, dy 114 — 44.5 degrees.

     That angle is now the site's one repeated graphic idea, so that the
     mark reads as the source of the design rather than as a favicon that
     happens to be lying around. It appears as: the corner notch on work
     cards and step markers, the hatch rule between sections, and the mark
     itself blown up behind the hero.

     One angle, used consistently, is a system. Two angles is a mess. If
     something new needs an angle, it uses this one. */
  --edge: 45deg;
  --notch: 18px;
}

/* Reset ------------------------------------------------------------------ */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
body {
  margin: 0;
  font-family: var(--font);
  font-size: var(--step-0);
  line-height: 1.65;
  color: var(--ink);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
}
img { max-width: 100%; height: auto; display: block; }
/* Headings move to DM Sans Bold. The face has a large x-height and fairly
   open counters, so it takes a tighter line-height and much tighter tracking
   at display sizes than the system stack did — 1.2/-0.02em looked loose the
   moment the font landed. The tracking is stepped: the bigger the type, the
   more it comes in, which is the normal optical correction. */
h1, h2, h3, h4 {
  font-family: var(--font-display);
  line-height: 1.08;
  margin: 0 0 var(--sp-s);
  color: var(--ink-deep);
  font-weight: 700;
  text-wrap: balance;
}
h1 { font-size: var(--step-4); letter-spacing: -0.035em; }
h2 { font-size: var(--step-3); letter-spacing: -0.03em; }
h3 { font-size: var(--step-1); letter-spacing: -0.015em; line-height: 1.2; }
h4 { font-size: var(--step-0); letter-spacing: -0.01em; line-height: 1.25; }
p  { margin: 0 0 var(--sp-s); max-width: 68ch; }
a  { color: var(--accent); }
a:hover { color: var(--accent-dark); }
a:focus-visible, button:focus-visible, input:focus-visible,
textarea:focus-visible, select:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 2px;
}
/* The accent ring is 5.4:1 on white but only 1.7:1 against the lightest part
   of the hero, which is where half the buttons on the home page live. Dark
   sections get a white ring instead: 8.9:1 at the hero's worst point. */
.hero a:focus-visible,
.page-hero a:focus-visible,
.contact-band a:focus-visible,
.site-footer a:focus-visible,
.skip:focus-visible {
  outline-color: #FFFFFF;
}
ul { padding-left: 1.2em; }
li { margin-bottom: .3rem; }

/* Layout ----------------------------------------------------------------- */
.wrap { width: 100%; max-width: var(--wrap); margin-inline: auto; padding-inline: var(--sp-s); }
@media (min-width: 700px) { .wrap { padding-inline: var(--sp-m); } }
/* ONE LEFT EDGE FOR THE WHOLE SITE.

   `.wrap--narrow` used to be a 740px container centred inside the page, which
   put every prose section 200px inside the header and footer. Scrolling down
   a page, the left edge stepped in and out: hero flush, prose indented, cards
   flush, prose indented. It read as a mistake rather than as a decision,
   because it is one - reading measure is a property of the LINE, not of the
   container, so narrowing the box was solving the wrong problem.

   Now the container runs the full page width and the blocks inside it are
   what carry the cap. Everything starts on the same left edge as the logo and
   the footer, and the text still stops at a readable length.

   `.prose p` and `.prose li` cap tighter still, at 70ch (about 718px), which
   is the number that actually governs comfortable reading. The 740 here is
   the outer bound for headings and non-prose blocks. */
.wrap--narrow { max-width: var(--wrap); }
.wrap--narrow > * { max-width: var(--wrap-narrow); }
.section { padding-block: var(--sp-xl); }
@media (min-width: 900px) { .section { padding-block: var(--sp-2xl); } }
.section--tint { background: var(--bg-tint); }
.section--pale { background: var(--accent-pale); }
.center { text-align: center; }
.center p { margin-inline: auto; }
/* A <p class="center"> is the centred element itself, not a child of one, so
   `.center p` never matches it. Without this its 68ch max-width pins it left
   and only the text inside centres, which reads as a misaligned button.
   Straight from the MCT stylesheet, where it bit once already. */
p.center { max-width: none; margin-inline: auto; }

/* The header is sticky, so anything jumped to by an in-page link would
   otherwise land underneath it. This includes #main, which the skip link
   targets, so without it the skip link appears to do nothing. */
[id] { scroll-margin-top: 5.5rem; }

/* Skip link -------------------------------------------------------------- */
.skip {
  position: absolute; left: -9999px; top: 0; z-index: 999;
  background: var(--ink-deep); color: #fff; padding: var(--sp-s) var(--sp-m);
}
.skip:focus { left: 0; }

/* Buttons ---------------------------------------------------------------- */
/* 0.85em padding on --step-0 text puts every button comfortably over the
   44px minimum touch target at every viewport width. */
.btn {
  display: inline-block;
  padding: 0.85em 1.9em;
  font-family: var(--font-display);
  font-size: var(--step-0);
  font-weight: 700;
  letter-spacing: -0.01em;
  line-height: 1.2;
  text-decoration: none;
  border-radius: var(--radius);
  border: 2px solid transparent;
  transition: background-color .15s ease, transform .15s ease, border-color .15s ease, color .15s ease;
}
.btn:hover { transform: translateY(-2px); }
.btn--primary { background: var(--accent); color: #fff; }
.btn--primary:hover { background: var(--accent-dark); color: #fff; }
/* The border is the only thing that identifies this as a button, so it has to
   clear 3:1 against white in its own right (WCAG 1.4.11). #7E8E96 is 3.39:1. */
.btn--ghost { background: transparent; color: var(--ink-deep); border-color: #7E8E96; }
.btn--ghost:hover { background: var(--ink-deep); color: #fff; border-color: var(--ink-deep); }
.btn--on-dark { background: #fff; color: var(--ink-deep); }
.btn--on-dark:hover { background: var(--accent-pale); color: var(--ink-deep); }

/* Header ----------------------------------------------------------------- */
.site-header {
  position: sticky; top: 0; z-index: 100;
  background: rgba(255,255,255,.97);
  backdrop-filter: saturate(150%) blur(6px);
  border-bottom: 1px solid var(--line);
}
.site-header__inner {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--sp-s);
  padding-block: 0.6rem;
}
.brand { display: flex; align-items: center; gap: 0.7rem; text-decoration: none; color: var(--ink-deep); }
/* The mark is the maunga from the logo, natural aspect 2:1. It is now an SVG
   traced from the original PNG (0.993 IoU against the source alpha), so it is
   sharp at any size, weighs 1.9 KB instead of 12 KB, and can be recoloured.
   The PNG is kept in assets/img as the fallback for the og-card pipeline. */
.brand img, .brand svg { width: 60px; height: 30px; object-fit: contain; }
.brand__text {
  font-family: var(--font-display);
  font-weight: 700; font-size: var(--step-0); line-height: 1.05;
  letter-spacing: -0.03em; text-transform: uppercase;
}
/* The second line is the one place the mono face carries a two-word phrase.
   At --step--2 with .16em tracking it reads as a stamp rather than as text,
   which is the intent. */
.brand__text span {
  display: block; font-family: var(--font-mono); font-weight: 700;
  font-size: var(--step--2); letter-spacing: .16em; color: var(--ink-soft);
  margin-top: .15em;
}

.nav-toggle {
  display: inline-flex; align-items: center; gap: .5rem;
  background: none; border: 2px solid #7E8E96; border-radius: var(--radius);
  padding: .55rem .8rem; font: inherit; font-size: var(--step--1); font-weight: 600;
  color: var(--ink-deep); cursor: pointer; min-height: 44px;
}
.nav-toggle__bars { display: block; width: 20px; height: 2px; background: currentColor; position: relative; }
.nav-toggle__bars::before, .nav-toggle__bars::after {
  content: ""; position: absolute; left: 0; width: 20px; height: 2px; background: currentColor;
}
.nav-toggle__bars::before { top: -6px; }
.nav-toggle__bars::after  { top: 6px; }

.nav { display: none; }
.nav[data-open="true"] {
  display: block;
  position: absolute; left: 0; right: 0; top: 100%;
  background: #fff; border-bottom: 1px solid var(--line);
  box-shadow: var(--shadow);
}
.nav ul { list-style: none; margin: 0; padding: var(--sp-s); display: grid; gap: 2px; }
.nav li { margin-bottom: 0; }
.nav a {
  display: block; padding: .8rem 1rem; text-decoration: none;
  color: var(--ink-deep); font-weight: 600; border-radius: var(--radius);
}
.nav a:hover { background: var(--accent-pale); color: var(--ink-deep); }
.nav a[aria-current="page"] { background: var(--accent-pale); box-shadow: inset 3px 0 0 var(--accent); }
.nav .nav__cta { display: block; margin-top: .4rem; }
.nav .nav__cta a { background: var(--accent); color: #fff; }
.nav .nav__cta a:hover { background: var(--accent-dark); color: #fff; }

@media (min-width: 800px) {
  .nav-toggle { display: none; }
  .nav { display: block !important; }
  .nav ul { display: flex; align-items: center; gap: .2rem; padding: 0; }
  .nav a { padding: .6rem .9rem; font-size: var(--step--1); }
  .nav a[aria-current="page"] { background: transparent; box-shadow: inset 0 -3px 0 var(--accent); border-radius: 0; }
  .nav .nav__cta { margin-left: .6rem; margin-top: 0; }
  .nav .nav__cta a { border-radius: var(--radius); }
  .brand img { width: 72px; height: 36px; }
}

/* The CTA keeps its solid fill even when it is the current page. Without
   this the aria-current rule (equal specificity, later in source) strips the
   background and leaves white text on white: an invisible button. Copied
   from MCT, where exactly that happened. */
.nav .nav__cta a[aria-current="page"] {
  background: var(--accent);
  color: #fff;
  box-shadow: none;
  border-radius: var(--radius);
}

/* Hero ------------------------------------------------------------------- */
/* The photo and the mark now carry the hero; the gradient is support.
   Previously this was text on a flat gradient and it was the weakest part of
   the site — the first thing anyone sees, doing nothing.

   CONTRAST IS UNCHANGED IN THE PLACES IT MATTERS. The two radial gradients
   keep their .18/.14 alphas, so the background still cannot get lighter than
   #135256 anywhere text sits, and every text colour used here still clears
   4.5:1 at that worst point. The mark watermark added below is capped at .07
   alpha and is checked in the same way. Raise any of those three numbers and
   the check has to be re-run. */
.hero {
  position: relative;
  background: var(--ink-deep);
  background-image:
    radial-gradient(60rem 30rem at 88% -10%, rgba(15,196,224,.18), transparent 65%),
    radial-gradient(45rem 28rem at 8% 110%, rgba(55,192,155,.14), transparent 62%);
  color: var(--on-dark);
  overflow: hidden;
}
/* The maunga, blown up and cropped off the left edge, sitting behind the
   headline. It is a mask rather than an <img> so it takes the cyan from the
   token and never loads a second file at a different colour.

   .07 alpha, chosen the same way everything else on this hero was: white
   body text on #135256 is 7.5:1, and lifting the plate under it by a 7%
   cyan wash costs about 0.4 of that. It is visible on a good screen and
   invisible on a bad one, which is the right side to err on.

   Hidden below 900px: at phone widths it sits directly under the headline
   and there is no room for it to read as anything but noise. */
.hero__mark {
  position: absolute;
  left: -8%; bottom: -18%;
  width: 52%; aspect-ratio: 2 / 1;
  background: var(--cyan);
  opacity: .07;
  -webkit-mask: url("../img/eswd-mark.svg") left bottom / contain no-repeat;
          mask: url("../img/eswd-mark.svg") left bottom / contain no-repeat;
  pointer-events: none;
  display: none;
}
@media (min-width: 900px) { .hero__mark { display: block; } }
.hero__inner { position: relative; padding-block: var(--sp-xl); }
@media (min-width: 900px) { .hero__inner { padding-block: var(--sp-2xl); } }
.hero h1 { color: #fff; max-width: 19ch; }
.hero__lead { font-size: var(--step-1); color: var(--on-dark); max-width: 50ch; margin-bottom: var(--sp-m); }
.hero__actions { display: flex; flex-wrap: wrap; gap: var(--sp-s); }
.hero__note { margin-top: var(--sp-m); font-size: var(--step--1); color: var(--on-dark-soft); }
.hero__note a { color: #fff; }

/* The brand rule, at the mark's angle -------------------------------------
   This used to be a flat 4px cyan-to-teal gradient. It is now the same
   gradient with a 45-degree hatch cut into it, so the one horizontal line on
   the page repeats the angle of the logo instead of ignoring it.

   The hatch is a repeating-linear-gradient used as a mask, which means it
   costs no image request and inherits the brand colours from the tokens.
   6px pitch: coarser and it reads as a barber's pole, finer and it turns to
   grey mush on a non-retina screen. */
.rule-brand {
  height: 6px; border: 0; margin: 0;
  background: linear-gradient(90deg, var(--cyan), var(--teal));
  -webkit-mask: repeating-linear-gradient(var(--edge), #000 0 3px, transparent 3px 6px);
          mask: repeating-linear-gradient(var(--edge), #000 0 3px, transparent 3px 6px);
}
/* A solid variant for the places where the hatch would be too busy — under a
   card, for instance, where it sits against a photograph. */
.rule-brand--solid { -webkit-mask: none; mask: none; height: 4px; }

/* Portrait --------------------------------------------------------------- */
/* One 4:5 image serves both the hero and the About page. On a phone it is
   cropped to a band with object-fit rather than hidden, because a photo of
   the person is the strongest trust signal on a one-person site and most of
   the traffic is on a phone. object-position keeps the face in frame when
   the band crops the top and bottom off. */
.portrait {
  width: 100%;
  border-radius: var(--radius);
  object-fit: cover;
  object-position: 50% 22%;
  max-height: 300px;
  box-shadow: var(--shadow);
}
@media (min-width: 850px) {
  .portrait { max-height: none; object-position: 50% 50%; }
}
/* On the dark hero the shadow does nothing, so a hairline edge separates the
   photo from the background instead. */
/* On the dark hero the shadow does nothing, so the photo gets the mark's
   angle instead: the top-right corner is cut at 45 degrees, which ties the
   one photograph on the page to the one geometric idea on the site. The
   hairline outline is dropped, because an outline follows the element's box
   and would draw straight across the notch. */
.hero .portrait {
  box-shadow: none;
  outline: none;
  border-radius: var(--radius);
  clip-path: polygon(
    0 0,
    calc(100% - var(--notch)) 0,
    100% var(--notch),
    100% 100%,
    0 100%
  );
}
/* The notch removes the rounded top-right corner, so the remaining three
   corners have to stay rounded on their own. clip-path and border-radius
   compose in that order, so this works: radius first, then the cut. */

.hero__inner .split { align-items: center; }
@media (min-width: 850px) {
  /* Was 1.35fr / 1fr. The photo was small enough to read as a stamp rather
     than as a person. At 1.15fr / 1fr the headline still wins the page —
     it is the largest thing on it by a distance — but the portrait is now
     big enough to do the job it is there for. */
  .hero__inner .split { grid-template-columns: 1.15fr 1fr; gap: var(--sp-xl); }
  .hero h1 { max-width: 15ch; }
  .hero__lead { max-width: 44ch; }
}

/* Small round portrait beside a name, the pattern Clearleft uses on their
   contact block. 96px display from a 260px file, so it stays sharp at 2x. */
.byline { display: flex; align-items: center; gap: var(--sp-s); margin-block: var(--sp-m); }
.byline img { width: 96px; height: 96px; border-radius: 50%; object-fit: cover; flex: none; }
.byline p { margin: 0; max-width: 44ch; }
.byline b { display: block; font-size: var(--step-0); }
.contact-band .byline img { outline: 2px solid rgba(255,255,255,.25); outline-offset: 2px; }

/* Eyebrow ---------------------------------------------------------------- */
/* Now set in IBM Plex Mono Bold. A mono face at small size, uppercase and
   tracked out, reads as a label rather than as a heading, which is exactly
   what an eyebrow is for — and it gives the site a second voice without
   adding a second display face.

   Tracking drops from .08em to .1em: mono is already evenly spaced, so it
   needs a little more air before it reads as tracked rather than as an
   accident. Size drops a step because mono runs wide and at --step--1 the
   longer eyebrows were wrapping at 375px.

   The 45-degree tick before it is the mark's angle again, at label scale. */
.eyebrow {
  display: inline-flex; align-items: center; gap: .55em;
  font-family: var(--font-mono);
  font-size: var(--step--2); font-weight: 700;
  letter-spacing: .1em; text-transform: uppercase; color: var(--accent);
  margin-bottom: var(--sp-s);
}
.eyebrow::before {
  content: ""; flex: none;
  width: .7em; height: .7em;
  background: currentColor;
  /* A right triangle at the mark's angle. Decorative only — the eyebrow's
     text is the content, and this carries no meaning a screen reader needs. */
  clip-path: polygon(0 100%, 100% 100%, 100% 0);
}
/* Not --cyan. The eyebrow sits inside the cyan-tinted corner of the hero
   gradient, where #0FC4E0 drops to 2.97:1 against its own tint. */
.hero .eyebrow, .page-hero .eyebrow { color: var(--cyan-pale); }

/* Cards ------------------------------------------------------------------ */
.grid { display: grid; gap: var(--sp-m); }
@media (min-width: 700px) { .grid--2 { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 900px) { .grid--3 { grid-template-columns: repeat(3, 1fr); } }

.card {
  background: #fff; border: 1px solid var(--line); border-radius: var(--radius);
  padding: var(--sp-m); box-shadow: var(--shadow);
}
.card h3 { margin-bottom: .4rem; }
.card p:last-child { margin-bottom: 0; }
.card__icon {
  width: 44px; height: 44px; border-radius: 50%;
  display: grid; place-items: center; margin-bottom: var(--sp-s);
  background: var(--accent-pale); color: var(--accent);
}
.card__icon svg { width: 24px; height: 24px; }

/* Split (media + text) --------------------------------------------------- */
.split { display: grid; gap: var(--sp-m); align-items: center; }
@media (min-width: 850px) {
  .split { grid-template-columns: 1fr 1fr; gap: var(--sp-l); }
  .split--reverse .split__media { order: 2; }
}
.split--top { align-items: start; }
.split__media img { border-radius: var(--radius); box-shadow: var(--shadow); width: 100%; }

/* Interior page hero ----------------------------------------------------- */
.page-hero {
  position: relative;
  background: var(--ink-deep);
  background-image: radial-gradient(55rem 26rem at 85% -20%, rgba(15,196,224,.26), transparent 62%);
  color: var(--on-dark);
  overflow: hidden;
}
.page-hero__inner { position: relative; padding-block: var(--sp-l) var(--sp-xl); }
@media (min-width: 900px) { .page-hero__inner { padding-block: var(--sp-xl) var(--sp-2xl); } }
.page-hero h1 { color: #fff; max-width: 20ch; }
.page-hero p { color: var(--on-dark); font-size: var(--step-1); max-width: 54ch; margin-bottom: 0; }

/* Service blocks --------------------------------------------------------- */
.service { padding-block: var(--sp-l); border-top: 1px solid var(--line); }
.service:first-of-type { border-top: 0; padding-top: 0; }
.service h2 { font-size: var(--step-2); }
.service ul { margin: 0 0 var(--sp-s); }
.service li { margin-bottom: .4rem; }

/* Price line ------------------------------------------------------------- */
.price {
  display: block;
  font-family: var(--font-display);
  font-size: var(--step-2); font-weight: 700; color: var(--ink-deep);
  letter-spacing: -0.03em; line-height: 1.15;
  font-variant-numeric: tabular-nums;
  border-left: 4px solid var(--accent);
  padding-left: var(--sp-s);
  margin-block: var(--sp-m);
  max-width: none;
}
.price small { display: block; font-size: var(--step--1); font-weight: 400; color: var(--ink-soft); margin-top: .2rem; }

/* Numbered steps --------------------------------------------------------- */
.steps { list-style: none; padding: 0; margin: 0; display: grid; gap: var(--sp-m); counter-reset: step; }
@media (min-width: 860px) { .steps--2col { grid-template-columns: repeat(2, 1fr); gap: var(--sp-l); } }
.steps > li {
  counter-increment: step; margin-bottom: 0;
  display: grid; grid-template-columns: 46px 1fr; gap: var(--sp-s); align-items: start;
}
/* Square with the corner cut, not a circle: the same 45-degree notch as the
   work cards and the hero portrait. Four of these run down the page, so it is
   the place the angle repeats most and does the most to make the site feel
   like one system rather than one nice hero. */
.steps > li::before {
  content: counter(step);
  display: grid; place-items: center;
  width: 46px; height: 46px; border-radius: 4px;
  clip-path: polygon(0 0, calc(100% - 12px) 0, 100% 12px, 100% 100%, 0 100%);
  background: var(--ink-deep); color: #fff;
  font-family: var(--font-display);
  font-weight: 700; font-size: var(--step-1);
  letter-spacing: -0.02em;
}
.steps h3 { font-size: var(--step-1); margin-bottom: .3rem; }
.steps p:last-child { margin-bottom: 0; }

/* Numbers band ----------------------------------------------------------- */
/* Every figure here is checkable, which is the entire point. Do not add one
   that is not: the site's argument is that it does not invent numbers. */
/* Left-aligned, not centred, and divided by rules. Three centred numbers in a
   row read as a brochure; three numbers on a baseline with a rule above each
   read as a ledger, which is closer to what this site is arguing. */
.stat-band { display: grid; gap: var(--sp-l); }
@media (min-width: 760px) { .stat-band { grid-template-columns: repeat(3, 1fr); gap: var(--sp-l); } }
.stat {
  margin: 0;
  max-width: none;
  padding-top: var(--sp-s);
  border-top: 3px solid var(--ink-deep);
}
.stat b {
  display: block;
  font-family: var(--font-display);
  /* A step above the h1 clamp's ceiling. These three numbers are the whole
     argument of the band, and DM Sans Bold's figures carry the size. */
  font-size: clamp(3rem, 2rem + 4.5vw, 5.2rem);
  line-height: .95;
  letter-spacing: -0.05em;
  font-variant-numeric: tabular-nums;
  color: var(--ink-deep);
}
.stat span {
  display: block; margin-top: .7rem;
  color: var(--ink-soft); max-width: 26ch;
}

/* Prose ------------------------------------------------------------------ */
.prose h2 { font-size: var(--step-2); margin-top: var(--sp-l); }
.prose h3 { margin-top: var(--sp-m); }
.prose p, .prose li { max-width: 70ch; }

/* Quotes ----------------------------------------------------------------- */
/* Real Google reviews, hand-authored into the HTML. Deliberately NOT a
   reviews widget: the Places API only returns five reviews and forbids
   caching them, so any widget has to call Google live on every page load.
   That would put a third-party request on a site whose Migrant Connections
   case study says there are none. Copy the text in by hand and link out to
   the profile for verification instead. */
.quote {
  border-left: 4px solid var(--accent);
  padding-left: var(--sp-m);
  margin-block: var(--sp-m);
}
.quote blockquote { margin: 0; }
.quote p { font-size: var(--step-1); color: var(--ink-deep); margin-bottom: .6rem; }
.quote cite { display: block; font-style: normal; font-size: var(--step--1); color: var(--ink-soft); }
.quote cite b { color: var(--ink-deep); }
/* --accent is 5.38:1 on white, so the stars carry their own contrast. The
   conventional gold does not: #B8860B is 4.0:1 and would fail. */
.stars { color: var(--accent); letter-spacing: .1em; margin: 0 0 .4rem; font-size: var(--step--1); }

.quote-grid { display: grid; gap: var(--sp-m); }
@media (min-width: 900px) { .quote-grid { grid-template-columns: repeat(3, 1fr); gap: var(--sp-l); } }
.quote-grid .quote { margin-block: 0; }
.quote-grid .quote p { font-size: var(--step-0); }

/* On a tinted section the left rule needs to stay legible against the tint;
   --accent is 5.00:1 on --bg-tint, so it does. */

/* Callout ---------------------------------------------------------------- */
.callout {
  background: var(--accent-pale); border-left: 4px solid var(--accent);
  padding: var(--sp-m);
  margin-block: var(--sp-m);
}
.callout :last-child { margin-bottom: 0; }
.callout h2, .callout h3 { font-size: var(--step-1); margin-top: 0; margin-bottom: .4rem; }

/* An unresolved fact, marked in the page rather than guessed at. Deliberately
   loud so it cannot reach the live site unnoticed. Delete the rule once the
   last [CONFIRM] is gone. */
/* Amber [CONFIRM] marker. The build notes said to delete this rule once the
   last marker was gone, which happened on 2026-08-27 when the SaygoWeb hosting
   location was resolved. Kept anyway, deliberately: with the rule present, any
   future [CONFIRM] span renders loud and yellow and cannot reach a client site
   unnoticed. Delete it and the next one renders as ordinary body text and slips
   through. Six lines of dead CSS is the cheaper failure mode. */
.confirm {
  display: inline-block;
  background: #FFF3CD; color: #5C4400;
  border: 1px dashed #8A6D00;
  padding: .15em .5em; border-radius: 4px;
  font-size: var(--step--1); font-weight: 600;
}

/* Contact block ---------------------------------------------------------- */
.contact-band { background: var(--ink-deep); color: var(--on-dark); }
.contact-band h2 { color: #fff; }
.contact-band p { color: var(--on-dark); }
.contact-lines { list-style: none; padding: 0; margin: 0 0 var(--sp-m); display: grid; gap: var(--sp-xs); }
.contact-lines li {
  margin-bottom: 0;
  font-family: var(--font-display);
  font-size: var(--step-2); font-weight: 700; letter-spacing: -0.03em;
}
.contact-lines a { color: #fff; text-decoration: none; border-bottom: 2px solid var(--cyan); }
.contact-lines a:hover { color: var(--cyan); }
.contact-band .btn { margin-top: var(--sp-xs); }

/* Footer ----------------------------------------------------------------- */
.site-footer { background: #08181D; color: var(--on-dark-soft); padding-block: var(--sp-xl) var(--sp-m); font-size: var(--step--1); }
/* The two column headings are labels, so they take the mono face like every
   other label on the site. The brand line stays in DM Sans: it is a name. */
.site-footer h2 { color: #fff; font-size: var(--step-1); margin-bottom: .6rem; }
.site-footer h3 {
  color: #fff; font-family: var(--font-mono); font-weight: 700;
  font-size: var(--step--2); letter-spacing: .12em; text-transform: uppercase;
  margin-bottom: .8rem;
}
.site-footer a { color: #fff; }
.site-footer a:hover { color: var(--cyan); }
.footer-grid { display: grid; gap: var(--sp-m); }
@media (min-width: 750px) { .footer-grid { grid-template-columns: 1.4fr 1fr 1fr; gap: var(--sp-l); } }
.site-footer ul { list-style: none; padding: 0; margin: 0; display: grid; gap: .45rem; }
.site-footer li { margin-bottom: 0; }
.footer-brand img { width: 84px; height: 42px; object-fit: contain; margin-bottom: var(--sp-s); }
.footer-brand p { color: var(--on-dark-soft); }
.footer-bottom {
  margin-top: var(--sp-m); padding-top: var(--sp-s);
  border-top: 1px solid rgba(255,255,255,.15);
  display: flex; flex-wrap: wrap; gap: var(--sp-s); justify-content: space-between;
}
.footer-bottom p { margin: 0; }

/* Forms ------------------------------------------------------------------ */
/* Same pattern as the MCT build, with one deliberate change: the input border
   there was #C3CBD8, which is 1.9:1 on white and fails WCAG 1.4.11 for a
   control boundary. Here it is #7E8E96 (3.39:1), the same value already used
   for the ghost button border. */
.form { display: grid; gap: var(--sp-m); max-width: 40rem; }
.field { display: grid; gap: .35rem; }
.field label { font-weight: 600; color: var(--ink-deep); }
.field .hint { font-size: var(--step--1); color: var(--ink-soft); }
.field input, .field textarea, .field select {
  font: inherit; font-size: var(--step-0); color: var(--ink);
  padding: .7em .85em; min-height: 48px;
  background: #fff;
  border: 2px solid #7E8E96; border-radius: var(--radius);
  width: 100%;
}
.field textarea { min-height: 160px; resize: vertical; line-height: 1.6; }
.field input:hover, .field textarea:hover, .field select:hover { border-color: var(--ink); }
.field input:focus, .field textarea:focus, .field select:focus {
  border-color: var(--accent);
}
.field .req { color: var(--accent); font-weight: 700; }
.form button[type="submit"] { justify-self: start; cursor: pointer; font-family: inherit; }
/* Honeypot. Off-screen rather than display:none, because some bots skip
   hidden fields but fill everything they can see in the DOM. */
.hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

/* Form failure banner. main.js reveals it when the URL carries
   ?status=error, but send-message.php also appends #form-error to the
   redirect, so this rule shows it even with JavaScript turned off. On the
   MCT build that banner was JS-only; this is the one improvement on it. */
#form-error:target { display: block !important; }

/* Work cards ------------------------------------------------------------- */
/* THE SCREENSHOTS ARRIVED. 28 Aug 2026.

   Each card now leads with a capture of the live client site, framed in a
   browser chrome bar drawn in CSS. The frame matters: a bare rectangle of
   someone else's web page reads as a stray image, and the same rectangle
   with a title bar and a domain on it reads unmistakably as "this is a site
   I built". It costs no image bytes and no extra request.

   WHAT COULD AND COULD NOT BE CAPTURED — read this before replacing one.
   MMAPS and Migrant Connections are Elijah's own static builds and they
   screenshot exactly as they look. FORM and TET are Avada/Fusion sites whose
   heroes are a background video and a lazy-loaded background image
   respectively; neither ever paints in a capture, so a homepage screenshot of
   either is a flat grey box. Both are therefore captured BELOW the fold,
   where the real content renders. That is a limitation of those two sites,
   not of the capture — do not keep retrying the hero.

   Each file is 1200px wide, JPEG q78 progressive, 31-93 KB, and every one is
   loading="lazy" below the first card.

   .work-card__panel is kept for any client with no usable capture. It is not
   dead CSS: FORM and TET are one theme update away from needing it back. */
.work-list { display: grid; gap: var(--sp-m); list-style: none; padding: 0; margin: 0; }
@media (min-width: 800px) { .work-list { grid-template-columns: repeat(2, 1fr); gap: var(--sp-l); } }
/* Three across on the home page. Declared after the two-column rule above so
   it wins at equal specificity; .grid--3 will not, because it is defined
   earlier in this file. */
@media (min-width: 900px) { .work-list--3 { grid-template-columns: repeat(3, 1fr); } }
.work-list > li { margin: 0; display: flex; }
.work-card {
  display: flex; flex-direction: column; width: 100%;
  background: #fff; border: 1px solid var(--line); border-radius: var(--radius);
  overflow: hidden; box-shadow: var(--shadow);
}
.work-card__panel {
  background: var(--ink-deep);
  /* Alpha matches .page-hero, so it inherits the same contrast check. */
  background-image: radial-gradient(30rem 16rem at 85% -25%, rgba(15,196,224,.26), transparent 62%);
  color: var(--on-dark);
  padding: var(--sp-m);
  display: grid; align-content: center; min-height: 148px;
  border-bottom: 4px solid var(--cyan);
}
/* Once the cards sit side by side, the panels have to be the same height or
   the cyan rules step up and down across the row. Measured on staging: a
   one-line name gives 148px and a two-line name 172-174px, so 180px holds
   every case and leaves headroom at the top of the clamp() scale. Below
   800px the cards are stacked, nothing has to line up, and the shorter
   148px is the better look on a phone. */
@media (min-width: 800px) { .work-card__panel { min-height: 180px; } }
/* FIXED 2026-09-09. This was `color: #fff` and had been since the cards led
   with the dark .work-card__panel. When the real screenshots replaced the
   panel on 28 August the name kept its white and lost its dark background,
   so every client name on the home page and the Work index has been white
   on a white card - invisible, while still taking up its 42px of height.
   .work-card__where was moved off the panel in that same round and had its
   colour corrected then; this one was missed.
   Default is now the dark heading colour, and the white is scoped to the
   panel, the same shape as the .work-card__where rule below. */
.work-card__name {
  margin: 0; color: var(--ink-deep); font-size: var(--step-2);
  letter-spacing: -0.02em; line-height: 1.15;
}
.work-card__panel .work-card__name { color: #fff; }
/* Now that the name sits on white in the card body rather than on the dark
   panel, the meta line under it needs a colour that passes on white.
   --cyan-pale is 1.5:1 there and would be unreadable; --accent is 5.38:1.
   The dark-panel variant keeps --cyan-pale, further down. */
.work-card__where {
  margin: .35rem 0 var(--sp-s); max-width: none;
  font-family: var(--font-mono);
  color: var(--accent); font-size: var(--step--2); font-weight: 700;
  letter-spacing: .1em; text-transform: uppercase;
}
.work-card__panel .work-card__where { color: var(--cyan-pale); margin-bottom: 0; }
.work-card__body { padding: var(--sp-m); display: flex; flex-direction: column; flex: 1; }
.work-card__body p { margin-bottom: var(--sp-s); }
.work-card__more { margin-top: auto; margin-bottom: 0; font-weight: 700; }

/* Screenshot in a browser frame ------------------------------------------ */
.work-card__shot { background: var(--ink-deep); }
.work-card__shot img {
  width: 100%; display: block;
  border-bottom: 4px solid var(--cyan);
}
/* The chrome bar. Deliberately generic — three dots and an address — because
   drawing a recognisable Chrome or Safari would be borrowing someone else's
   trade dress to sell web design, which is the wrong look on this site of all
   sites.

   aria-hidden in the markup: the domain is decoration here, and the card
   already names the client in its <h3> and links out in the body. */
.browser-bar {
  display: flex; align-items: center; gap: .45rem;
  padding: .55rem .8rem;
  background: #12333C;
  border-bottom: 1px solid rgba(255,255,255,.1);
}
.browser-bar i {
  width: 9px; height: 9px; border-radius: 50%; flex: none;
  background: rgba(255,255,255,.28);
}
.browser-bar b {
  margin-left: .5rem;
  font-family: var(--font-mono); font-weight: 700;
  font-size: var(--step--2); letter-spacing: .04em;
  /* --on-dark-soft is 8.9:1 on #12333C. It is decorative and aria-hidden, so
     it has no contrast obligation at all, but a label nobody can read is
     still a bad label. */
  color: var(--on-dark-soft);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* The card as a whole gets the mark's angle on its top-right corner, the
   same cut as the hero portrait. This is why .work-card already sets
   overflow: hidden — the screenshot has to be clipped by it. */
.work-card {
  clip-path: polygon(
    0 0,
    calc(100% - var(--notch)) 0,
    100% var(--notch),
    100% 100%,
    0 100%
  );
  /* CLIP-PATH CLIPS THE BOX-SHADOW TOO. The shadow set on .work-card above
     is drawn outside the element's box and is therefore cut away entirely by
     the polygon, which is why the shadow moves to the parent as a
     drop-shadow filter below. Leaving box-shadow here would be dead CSS that
     reads as if it worked, so it is switched off explicitly. */
  box-shadow: none;
}
/* filter: drop-shadow() follows the alpha of what was actually painted, so it
   traces the notched outline instead of the element's rectangle. Values match
   --shadow: the same two layers, so cards look identical to .card elsewhere
   on the page apart from the cut corner.

   It goes on the <li>, not on .work-card, because a filter on the clipped
   element would itself be clipped. */
.work-list > li {
  filter:
    drop-shadow(0 2px 4px rgba(12,35,42,.05))
    drop-shadow(0 10px 30px rgba(12,35,42,.07));
}
/* clip-path also clips the 1px card border along the cut, so the diagonal
   edge is drawn by the drop-shadow alone. That is the intent on white and on
   --bg-tint, which are the only two backgrounds a work card sits on. */

/* Case studies ----------------------------------------------------------- */
.meta-list {
  list-style: none; padding: 0; margin: 0 0 var(--sp-m);
  display: grid; gap: .5rem;
}
.meta-list li { margin: 0; }
/* The label half of each "Client: MMAPS" pair, set as a label like every
   other one on the site. */
.meta-list b {
  font-family: var(--font-mono); font-weight: 700;
  font-size: var(--step--2); letter-spacing: .1em; text-transform: uppercase;
  color: var(--ink-deep);
}
.page-hero .meta-list li { color: var(--on-dark); }
.page-hero .meta-list b { color: #fff; }

.figure { margin: var(--sp-l) 0; }
.figure img { width: 100%; border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow); }
.figure figcaption { margin-top: .7rem; font-size: var(--step--1); color: var(--ink-soft); max-width: 70ch; }

/* A figure whose image is a screenshot of a website rather than a photograph.
   Same browser frame as the work cards, so a capture looks the same wherever
   it appears on the site. The <img> loses its own border and radius because
   the wrapper carries them now. */
.figure--screen .screen {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  overflow: hidden;
  background: var(--ink-deep);
}
.figure--screen .screen img {
  border: 0; border-radius: 0; box-shadow: none;
  border-bottom: 4px solid var(--cyan);
}

.did-list li { margin-bottom: .7rem; }

.case-nav {
  display: flex; flex-wrap: wrap; gap: var(--sp-s); justify-content: space-between;
  margin-top: var(--sp-l); padding-top: var(--sp-m);
  border-top: 1px solid var(--line);
}

/* Plain client list. No logos: the ones on file are inconsistent in quality
   and format, and a bad logo strip reads worse than none. */
.client-list {
  list-style: none; padding: 0; margin: 0;
  display: flex; flex-wrap: wrap; gap: .5rem .75rem;
}
.client-list li {
  margin: 0; font-size: var(--step--1); font-weight: 600; color: var(--ink-deep);
  background: var(--accent-pale);
  /* The chip's fill is only 1.06:1 against the tinted section behind it, so
     the border is what actually draws the chip. #BFDDE4 was 1.33:1 and failed
     WCAG 1.4.11; #628F9A is 3.30:1 on the tint and 3.55:1 on white, so the
     chip reads on either background. */
  border: 1px solid #628F9A;
  border-radius: 999px; padding: .35rem .85rem;
}

/* Logo strip ------------------------------------------------------------- */
/* Every logo is area-normalised at build time, not sized here. A tall crest
   and a long wordmark set to the same CSS height look nothing alike; scaled
   to the same area they read as equal weight. Each img therefore carries its
   own width and height at exactly half the source, so the balance survives.
   Do not add a blanket height to these or it all comes undone.

   No greyscale filter, on purpose. The convention is to grey a logo strip
   out, but FORM's wordmark is already light grey and Kitchen Draw and Central
   Taranaki Automotive are carried by their orange. Greyed and faded, half of
   them disappear. Colour at a consistent size is honest and legible.

   Sits on a white section because the Central Taranaki Automotive asset is a
   JPEG with a white background baked in. If this ever moves to a tinted or
   dark band, that logo needs a transparent version first. */
/* A grid, not flex-wrap.

   Flex-wrap was the first attempt and it broke 4 + 2 at desktop: six logos
   whose widths run from 78px to 221px do not divide evenly, so the last row
   was left with two orphans sitting off-centre. Wrapping was deciding the
   composition, and it decided badly.

   A grid fixes the number of columns instead, so the rows are always even:
   3 + 3 at desktop, 2 + 2 + 2 on a large phone, and a single column below
   540px. Each logo is centred in its own cell, which also means a logo can be
   swapped for a wider one without re-flowing the row. Keep the count
   divisible by the column count - add a seventh logo and the grid orphans it,
   same as flex did.

   THE BREAKPOINTS ARE 540 AND 800, AND THEY ARE NOT ROUND NUMBERS BY
   ACCIDENT. The widest logo is 221px. A cell narrower than that makes
   img { max-width: 100% } shrink it, which quietly undoes the area
   normalisation the logos were built to. 540px is the narrowest viewport
   where two columns still clear 221px (cells land at 232px); 800px is the
   narrowest where three do (224px). At the obvious 480 and 700 the widest
   three logos were being squeezed to 202px and 200px respectively.

   Checked at 320, 375, 414, 500, 540, 600, 700, 768, 800, 900, 1024 and
   1280: no horizontal overflow and no logo rendered below its natural size
   at any of them. If you change a breakpoint or add a wider logo, re-run
   that check rather than eyeballing one width. */
.logo-strip {
  display: grid;
  grid-template-columns: 1fr;
  place-items: center;
  gap: var(--sp-l) var(--sp-m);
  list-style: none; padding: 0; margin: 0;
}
@media (min-width: 540px) {
  .logo-strip { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 800px) {
  .logo-strip { grid-template-columns: repeat(3, 1fr); gap: var(--sp-xl) var(--sp-m); }
}
.logo-strip li { margin: 0; }
.logo-strip img { display: block; max-width: 100%; height: auto; }

/* Breakout --------------------------------------------------------------- */
/* Lets a non-prose block use the full column instead of stopping at reading
   measure. A chip cloud, a figure or a table is not prose and should not be
   held to a width sized for sentences.

   This used to be a negative margin: `margin-inline: (740 - 1140) / 2`, which
   dragged the block 200px out of a centred narrow column, and only from
   1200px up because below that the pull caused overflow. That whole mechanism
   is gone. Now that `.wrap--narrow` is full width and the cap lives on the
   children, a breakout is simply a child that opts out of the cap - no
   negative margins, no minimum viewport, and it behaves the same at every
   width.

   THIS RULE HAS TO LIVE AT THE END OF THE FILE. `.wrap--narrow > *` is a
   class plus a universal selector, so it has the same specificity as
   `.breakout`, and source order is the only thing that decides. The same trap
   caught the old version: `.client-list` and `.figure` set their own margins
   further down than the Prose section this originally sat in, so the breakout
   was silently doing nothing. Put a utility after the components it
   modifies. */
.breakout { max-width: none; margin-inline: 0; }

/* Utility ---------------------------------------------------------------- */
.visually-hidden {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.lede { font-size: var(--step-1); color: var(--ink-soft); }
.nowrap { white-space: nowrap; }

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

/* Compact work cards, the tile strip on the Work page --------------------
   Added 2026-09-09.

   These are a deliberately QUIETER version of .work-card, not a second
   gallery. The Work page leads with four case studies; this strip sits below
   them inside "the rest of the book" and exists to show the kind of business
   rather than to tell another story. Research on portfolios is consistent
   that three to six deep case studies beat twenty shallow tiles, so if this
   strip ever starts competing with the cards above it for attention, it has
   gone wrong. Keep it smaller.

   It reuses .work-card wholesale, which is the point: the 45-degree notch,
   the drop-shadow on the parent li, the .browser-bar and the cyan rule all
   come for free and cannot drift out of step with the case studies.

   MUST STAY AT THE END OF THE FILE. .work-card__name and .work-card__body are
   declared earlier at the same specificity, so source order is the only thing
   deciding this. That trap has already bitten .breakout twice; read the note
   above it. */
.work-card--compact .work-card__body { padding: var(--sp-s) var(--sp-m) var(--sp-m); }
.work-card--compact .work-card__name { font-size: var(--step-0); line-height: 1.25; }
.work-card--compact .work-card__where { margin-bottom: var(--sp-xs); }
.work-card--compact .work-card__more { font-size: var(--step--1); }

/* The tile images are 3:2 and the case-study shots are not, so the aspect
   ratio is pinned here rather than left to the intrinsic size. Without it a
   slow connection reflows the whole strip when each image lands. */
.launch-list .work-card__shot img { aspect-ratio: 3 / 2; object-fit: cover; }

/* FOUR ACROSS, NOT THREE, AND THE REASON IS ARITHMETIC.
   There are eight tiles. In a three-column grid that is 3 + 3 + 2 and the
   last row is left with two orphans and a hole where the third should be.
   This site has already been bitten by exactly that: the six-logo strip on
   the home page broke 4 + 2 under flex-wrap, and the fix recorded in
   BUILD-NOTES was to stop letting the wrap decide the composition and fix
   the column count instead.

   Eight divides evenly by four and by two, so every row is full at every
   breakpoint: 4 + 4 on desktop, 2 + 2 + 2 + 2 on a tablet, one column on a
   phone. IF YOU ADD OR REMOVE A TILE, CHECK THE ARITHMETIC AGAIN. Nine tiles
   want three columns; ten want five or two. A row with a hole in it reads as
   a bug even when nothing is broken.

   Declared after .work-list and .work-list--3 so it wins at equal
   specificity, same reason as .breakout. */
/* minmax(0, 1fr) AND NOT 1fr. Measured on staging before this was deployed:
   with repeat(4, 1fr) the four columns came out 247.8, 329.7, 284.2 and
   293.3px instead of equal quarters. The reason is that 1fr is shorthand for
   minmax(auto, 1fr), and auto refuses to go below the content's minimum
   width. The domains in the browser bars are long unbreakable strings -
   taranakirefrigeration.co.nz is the worst - so each column was being pushed
   out to fit its own bar and the row lost its rhythm.
   minmax(0, 1fr) lets the track shrink past the content, and the overflow is
   handled on .browser-bar b below. Same trap applies to any grid on this site
   that contains a URL, an email address or a long client name. */
.launch-list { grid-template-columns: minmax(0, 1fr); gap: var(--sp-m); }
@media (min-width: 620px)  { .launch-list { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 1000px) { .launch-list { grid-template-columns: repeat(4, minmax(0, 1fr)); } }

/* With the columns now allowed to shrink, a long domain has to be told what
   to do rather than left to overflow its bar. Truncating is right here: the
   bar is decorative chrome marked aria-hidden, the full domain is in the
   visible link underneath, so nothing is lost to anyone. */
.launch-list .browser-bar b {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;
}
