/* Fire Mountain Labs · firemountainlabs.com
   Design system v2: "Summit Station", approved 2026-07-28 via dot-voting.
   Palette (official brand card): Cocoa Brown #cb6929 · Prussian Blue #003452
   · Timberwolf #cfcfcf · Burgundy #74121d · Viridian #56876d. Darks are
   shades of the Prussian Blue hue.
   Type: Archivo display · Public Sans body · IBM Plex Mono labels. */

@import url('/assets/fonts/fonts.css');

:root {
  --navy: #003452;        /* brand */
  --n-900: #00131f;       /* deepest ground: footer, insights */
  --n-800: #001a2b;       /* hero field */
  --n-700: #002236;       /* section ground */
  --n-600: #002b42;       /* card ground */
  --orange: #cb6929;      /* Cocoa Brown: the signal color, CTAs and labels only */
  /* Cocoa Brown is 3.76:1 on white: a fill, border and large-type color, never
     body text. --orange-deep is Cocoa Brown pressed down to 4.97:1, and carries
     both jobs the lighter shade cannot: orange words at body and small sizes on
     light grounds, and the CTA hover fill, where white sits on it at the same
     4.97:1. --orange-soft is the counterpart on dark. Check the ratio before
     lightening any of them. See .claude/brand-voice-guidelines.md.

     --orange-deep is 4.42:1 on --paper-2, under the floor. That ground is only
     reachable through .section--mist, which no page currently uses. If that
     section ever ships with orange text on it, this value has to darken to
     #a95722 (4.58:1 there). */
  /* The CTA fill moved from --orange to --orange-deep: white on Cocoa Brown is
     3.76:1, and .btn sets 0.9rem bold (14.4px), under the 18.66px that would
     make it large text, so the floor is 4.5:1 and the button missed it.
     --orange-press is Cocoa Brown pressed one step further, 6.56:1 on white,
     so the hover state stays darker than the resting state. */
  --orange-press: #94481b; /* CTA hover fill, 6.56:1 with white */
  --orange-deep: #b15620; /* orange on light, readable; and the CTA hover fill */
  --timberwolf: #cfcfcf;  /* light neutral */
  --burgundy: #74121d;    /* severity / critical accents */
  /* Burgundy is the inverse of the other accents: 11.37:1 on white, so it needs
     no darker variant, but 1.30:1 on --n-600 and 1.66:1 on --n-900, the worst
     figure any palette color posts on any ground. Without a light counterpart,
     severity could not be written as text in a dark section, and most of this
     site's sections are dark. Reaching the floor from #74121d against a dark
     ground takes a large lightness move, so --burgundy-soft is a lighter, pinker
     red than the palette value by necessity, not by preference; it is the
     dark-ground voice of Burgundy, never a second brand color, and never used on
     light, where it falls to 3.18:1. Hue and saturation are preserved
     (353 degrees, 73 percent). */
  --burgundy-soft: #e76775; /* burgundy words on dark, 4.63:1 on --n-600 */
  --viridian: #56876d;    /* success / remediation accents */
  /* Viridian is 4.13:1 on white and 3.57:1 on --n-600, so the palette value is
     a fill, border and large-type color on either ground. It clears the text
     floor only on --n-900 (4.57:1), which is too narrow a target to rely on, so
     it gets the same two-token treatment as the orange: -deep for words on
     light, -soft for words on dark. Both preserve Viridian's hue and saturation
     (148.2 degrees, 22.2 percent). See .claude/brand-voice-guidelines.md.

     Same --paper-2 caveat as --orange-deep: --viridian-deep is 4.13:1 there,
     and #4c7760 is the answer if .section--mist ever ships with text on it. */
  --viridian-deep: #507e66; /* viridian words on light, 4.65:1 on --paper */
  --viridian-soft: #639c7e; /* viridian words on dark, 4.63:1 on --n-600 */
  --orange-soft: #f0a877; /* orange on dark, readable */
  --steel: #7f97a8;       /* muted on dark */
  --steel-2: #9db3c0;     /* small print on dark: insights, ledger, sessions */
  --steel-3: #5d7180;     /* quietest step on dark: currently unreferenced */
  --mist: #cfdde6;        /* body text on dark */
  --mist-2: #a8c0cf;      /* card and panel body on dark */
  --mist-3: #e2ecf2;      /* nav links */
  --mist-4: #d7e2ea;      /* services ribbon */
  /* The quiet contour lines in the hero field. Same 204deg hue as --navy and
     the whole steel/mist ladder, just carrying more saturation than any of
     them: 48% against --mist-2's 29%. Read by assets/js/main.js, which draws
     it at a per-line alpha, so it never appears in a stylesheet rule. */
  --contour: #8cb9d7;
  --paper: #ffffff;
  --paper-2: #eef2f5;     /* light section ground */
  --ink: #16222c;         /* body text on light */
  --ink-2: #41586a;       /* secondary on light */
  --line-l: var(--timberwolf);      /* hairline on light */
  --line-d: rgba(255, 255, 255, 0.12);       /* default hairline on dark */

  /* Translucent values, previously written as rgba() literals at 18 sites.
     The white hairlines had drifted to six alphas (0.05 0.08 0.09 0.12 0.14
     0.15 0.16) for what reads as one idea, so they collapse to three steps:
     0.09 joins 0.08, and 0.14 and 0.16 join 0.15. The largest move is 0.01 of
     alpha on a 1px line, below the threshold of visible difference. The orange
     and scrim values keep their exact previous alphas. */
  --line-d-soft: rgba(255, 255, 255, 0.08);  /* quieter divider */
  --line-d-firm: rgba(255, 255, 255, 0.15);  /* card and panel edge */
  --wash-d: rgba(255, 255, 255, 0.05);       /* faint panel fill */
  --orange-wash: rgba(203, 105, 41, 0.14);   /* nav CTA fill */
  --orange-line: rgba(203, 105, 41, 0.45);   /* featured headline border */
  --orange-glow: rgba(203, 105, 41, 0.22);   /* radial glow, inner stop */
  --orange-glow-mid: rgba(203, 105, 41, 0.08);
  --orange-glow-0: rgba(203, 105, 41, 0);    /* radial glow, clear stop */
  --shadow: rgba(0, 10, 18, 0.5);            /* drop shadow */
  --scrim: rgba(0, 19, 31, 0.62);            /* flat scrim over imagery */
  --scrim-0: rgba(0, 19, 31, 0);             /* scrim gradient, clear end */
  --scrim-deep: rgba(0, 19, 31, 0.88);       /* scrim gradient, solid end */

  --font-display: 'Archivo', 'Helvetica Neue', Arial, sans-serif;
  --font-body: 'Public Sans', 'Helvetica Neue', Arial, sans-serif;
  --font-mono: 'IBM Plex Mono', ui-monospace, Menlo, monospace;

  --container: 1160px;

  /* ---- corner radius ----
     Every radius on the site is one step off --r-unit, so the whole thing is
     dialled from this one number:

       --r-unit: 0     square, exactly as the site shipped before this
       --r-unit: 2px   softened: the value here
       --r-unit: 4px   the first mockup. Reviewed 2026-08-01 and turned down:
                       it reads as a product marketing page, and the square
                       edge was carrying brand weight on a site that sells
                       audits and red teaming
       --r-unit: 7px   chips read as lozenges, cards read as app tiles

     Five steps, assigned by the size of the box rather than by what the box
     is: a 6px list marker and a 1160px panel cannot carry the same radius and
     still look drawn by the same hand, but everything between them sorts into
     small, medium and large. --r-full is capped by the box, so it is the pill,
     and it scales too rather than being a literal, or 0 would not square it. */
  --r-unit: 2px;
  --r-xs: calc(var(--r-unit) * 0.75);  /* 1.5px · list markers */
  --r-sm: calc(var(--r-unit) * 1.5);   /*   3px · thumbnails, in-card images */
  --r-md: calc(var(--r-unit) * 2.5);   /*   5px · the default: buttons, cards, panels, embeds */
  --r-lg: calc(var(--r-unit) * 4);     /*   8px · large panels and portraits */
  --r-xl: calc(var(--r-unit) * 6);     /*  12px · the sheet over the hero */
  --r-full: calc(var(--r-unit) * 999); /* pills: tags, dots, rule caps */

  /* header lockup height: 75px, shrinking on phones so it clears the menu button */
  --lockup-h: min(75px, 14vw);
  /* header height: the lockup with 20px clear above and below */
  --header-h: calc(var(--lockup-h) + 40px);

  /* The type scale, restated in points 2026-07-31. Body text is 12pt and the
     smallest type on the site is 10pt; every other step is a whole number of
     points measured from the 12pt body. The previous scale ran 9pt to 43.2pt
     on no stated system, and its two small steps (9pt and 10.5pt) both sat
     under the new floor.

     Written in rem, not pt. 1rem is 12pt at default settings, so the point
     values below are the design intent, not a lock: a reader who raises their
     browser's base size scales the whole ladder with it, which an absolute pt
     unit would refuse to do.

     --step--2 (10pt) is the floor. Nothing on the site may set a font-size
     below it, and it is for footnotes and the mono eyebrows: type that is
     short, scanned rather than read, and used judiciously. Anything carrying a
     sentence starts at --step--1 (11pt). Reach for the token, not a literal,
     so the floor stays a single place to raise. */
  --step-5: clamp(2.58333rem, 1.7rem + 3vw, 3.58333rem);  /* 31 → 43pt */
  --step-4: clamp(1.91667rem, 1.4rem + 1.6vw, 2.5rem);    /* 23 → 30pt */
  --step-3: clamp(1.5rem, 1.25rem + 0.9vw, 1.91667rem);   /* 18 → 23pt */
  --step-2: 1.33333rem;   /* 16pt */
  --step-1: 1.08333rem;   /* 13pt */
  --step-0: 1rem;         /* 12pt · body */
  --step--1: 0.91667rem;  /* 11pt · small text, and any label above a footnote */
  --step--2: 0.83333rem;  /* 10pt · the floor: footnotes and mono eyebrows */
}

/* ---------- base ---------- */

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

html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }

body {
  margin: 0;
  font-family: var(--font-body);
  font-size: var(--step-0);
  line-height: 1.65;
  color: var(--ink);
  /* The canvas behind the page, not the page: every section paints its own
     ground and the footer closes every page on --n-900, so the only places
     this shows are the overscroll gutter and the strip below a page shorter
     than the viewport. Both were white, which flashed against a site that is
     dark end to end. */
  background: var(--n-900);
}

img { max-width: 100%; height: auto; display: block; }

/* Images ship as <picture><source type="image/webp"><img></picture>, which puts
   a wrapper element between the image and whatever laid it out. `display:
   contents` takes that wrapper out of the box tree, so the <img> stays the flex
   item of .media-rail and .author-chip, the grid item of .author-card, and the
   thing .prose img's 28px rhythm margin applies to. Without it the picture is
   an inline box that swallows those margins and collapses the image to a
   shrink-to-fit line box.

   It does not fix selectors: `display: contents` changes layout, not matching,
   so every `>` combinator that used to reach an <img> now stops at the picture.
   Those rules are extended one at a time below, each keeping its own
   specificity rather than being loosened to a descendant selector. */
picture { display: contents; }

/* The other half of that trade. `display: contents` promotes *every* child of
   the picture into the parent's layout, and a picture has two: the <source> as
   well as the <img>. A <source> paints nothing, but Blink and WebKit still
   blockify it into a real zero-width flex item or a real grid cell, so each one
   silently eats a gap it was never meant to occupy. In the footer that doubled
   the 28px between the roundel and the wordmark to 56px, and put a phantom
   ahead of the first SBA badge, which pushed the pair 8px right of the centre
   line every other row in that column is centred on: the misalignment is
   obvious on a phone, where the row stacks under centred text. Take the
   <source> back out of the box tree. It is metadata, and the srcset it feeds is
   chosen by HTML's image-selection algorithm, which never consults CSS. */
source { display: none; }

h1, h2, h3, h4 {
  font-family: var(--font-display);
  font-weight: 700;
  line-height: 1.15;
  color: var(--navy);
  margin: 0 0 0.6em;
  letter-spacing: -0.02em;
}

h1 { font-size: var(--step-4); }
h2 { font-size: var(--step-3); }
h3 { font-size: var(--step-2); }

p { margin: 0 0 1em; }

a { color: var(--orange-deep); text-decoration: none; }
a:hover { text-decoration: underline; }

:focus-visible { outline: 2px solid var(--orange); outline-offset: 2px; }

.container { max-width: var(--container); margin-inline: auto; padding-inline: 24px; }

.skip-link {
  position: absolute; left: -9999px; top: 0;
  background: var(--navy); color: #fff; padding: 10px 18px; z-index: 100;
  border-radius: var(--r-md);
}
.skip-link:focus { left: 0; }

/* mono eyebrow / section label. Element-agnostic on purpose: a section label
   that names the section is the section's heading, so it also runs as an <h2>,
   and the weight and line height are stated here rather than inherited so the
   two elements render identically. */
.seclbl {
  font-family: var(--font-mono);
  font-size: var(--step--2);
  font-weight: 400;
  line-height: 1.65;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--orange-deep);
  margin: 0 0 10px;
}
/* .seclbl also runs on dark grounds, including inside .atlas, where the
   light-ground token would fall to 3.2:1. */
.section--deep .seclbl, .section--night .seclbl, .atlas .seclbl { color: var(--orange-soft); }

/* A section label that names its section is that section's heading, and the
   markup already says so: three of them run as <h2> (home's "Where to start"
   and "Media", publications' "Selected Publications") while every other
   .seclbl is a <p> or <span> eyebrow inside a card. Only the headings take
   this: --step--1 to match .ribbon-label, which heads the services strip at
   the same rank and was the one label on the home page reading at the right
   size, and bold so a header outranks any kicker sitting under it. The
   in-card eyebrows stay at the smaller size: they label a panel, not a
   section. */
h2.seclbl, .ribbon-label { font-size: var(--step--1); font-weight: 700; }

/* ---------- header / nav ---------- */

.site-header {
  background: var(--n-800);
  position: relative;
  z-index: 50;
}

/* Every page floats its header over the contour hero field */
.site-header--overlay { position: absolute; inset: 0 0 auto; background: transparent; }

.site-header .container {
  display: flex;
  align-items: center;
  gap: 24px;
  min-height: var(--header-h);
}

.brand { display: inline-flex; align-items: center; flex-shrink: 0; }
/* the lockup is ~5:1, so a flat 75px would overrun the menu button on phones */
.brand img, .brand svg { height: var(--lockup-h); width: auto; display: block; }

.nav-toggle { display: none; background: none; border: 0; padding: 8px; cursor: pointer; margin-left: auto; }
.nav-toggle .bar { display: block; width: 24px; height: 2px; background: #fff; margin: 5px 0; border-radius: var(--r-full); }

.site-nav { margin-left: auto; }
.site-nav ul { list-style: none; margin: 0; padding: 0; }
.site-nav > ul { display: flex; align-items: center; gap: 6px; }

.site-nav a {
  display: block;
  color: var(--mist-3);
  font-weight: 600;
  font-size: var(--step--1);
  padding: 26px 14px;
  text-decoration: none;
  white-space: nowrap;
}
.site-nav a:hover { color: var(--orange-soft); text-decoration: none; }
.site-nav > ul > li > a[aria-current="page"],
.site-nav > ul > .current > a { box-shadow: inset 0 -3px 0 var(--orange); }

.has-sub { position: relative; }
.has-sub > a::after {
  content: "";
  display: inline-block;
  margin-left: 7px;
  border: 4px solid transparent;
  border-top-color: var(--steel);
  vertical-align: 2px;
}

.sub-menu {
  position: absolute;
  top: 100%;
  left: 0;
  min-width: 240px;
  background: var(--n-700);
  border: 1px solid var(--line-d);
  border-top: 3px solid var(--orange);
  box-shadow: 0 14px 34px var(--shadow);
  padding-block: 6px;
  display: none;
  /* No overflow:hidden to clip the hover fills into the corners: the second
     level is a child positioned at left:100% and hiding the overflow would
     clip it away entirely. The 6px padding-block already keeps the first and
     last item's fill clear of most of the arc. */
  border-radius: var(--r-md);
}
.has-sub:hover > .sub-menu,
.has-sub:focus-within > .sub-menu,
.has-sub.open > .sub-menu { display: block; }

.sub-menu a {
  color: var(--mist);
  font-weight: 400;
  font-size: var(--step--1);
  padding: 9px 18px;
  white-space: normal;
}
.sub-menu a:hover { color: #fff; background: var(--n-600); }

.sub-menu .has-sub > a::after { transform: rotate(-90deg); vertical-align: 0; }
.sub-menu .sub-menu { top: -9px; left: 100%; min-width: 300px; }

.nav-cta {
  margin-left: 10px;
  border: 1px solid var(--orange);
  background: var(--orange-wash);
  border-radius: var(--r-md);
}
.nav-cta > a { color: var(--orange-soft) !important; font-weight: 700; padding: 9px 16px; }
.nav-cta > a:hover { color: #fff !important; }

/* ---------- hero: contour field spans nav + hero, every page ---------- */

.hero-field { position: relative; overflow: hidden; background: var(--n-800); }

/* The one rounded corner on anything full-bleed, and it belongs to the section
   below rather than to the hero. Rounding the hero's own bottom was the first
   attempt and it does not work: the corner then shows whatever is behind the
   hero, which is the body ground, darker than every section that follows it,
   so each corner reads as a dark wedge rather than as a curve.

   Inverted, it does. The section takes the radius on its top two corners and
   is pulled up by exactly that radius, so it sits on the hero as a sheet and
   the curve is filled by the hero's own contour field. Every page that has a
   hero is followed by .section--deep or, on the home page, by .ribbon, and
   both paint a ground, which is what this needs to have anything to round.

   The 12px it steals back is hero padding: the hero closes 72px under its last
   line, so nothing moves. */
.hero-field + * {
  position: relative;
  z-index: 3;
  border-radius: var(--r-xl) var(--r-xl) 0 0;
  margin-top: calc(-1 * var(--r-xl));
}
/* The contours are drawn into a fixed 1440x640 bitmap and the field is
   content-height, so the box is never the bitmap's aspect ratio. Without
   object-fit the bitmap stretches to fit and the ridges flatten;
   cover keeps them at the drawn proportions and crops the overflow instead. */
.hero-field > canvas {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  opacity: 0.55;
}
/* overlay header sits above the hero, so the hero clears it, then breathes */
.hero-field .hero { padding-top: calc(var(--header-h) + 72px); }

.hero {
  position: relative;
  z-index: 2;
  padding: 64px 0 78px;
  color: #fff;
}
.hero h1 {
  font-size: var(--step-5);
  font-weight: 800;
  color: #fff;
  letter-spacing: -0.028em;
  margin-bottom: 0.35em;
}
.hero .lead {
  font-size: var(--step-1);
  line-height: 1.6;
  color: var(--mist);
  max-width: 46ch;
  margin: 0 0 30px;
}

/* ---------- buttons ---------- */

.btn {
  display: inline-block;
  font: 700 0.9rem/1 var(--font-body);
  padding: 15px 30px;
  border: 1px solid var(--orange-deep);
  color: #fff;
  background: var(--orange-deep);
  text-align: center;
  cursor: pointer;
  border-radius: var(--r-md);
}
.btn:hover { background: var(--orange-press); border-color: var(--orange-press); text-decoration: none; }

/* Square, alone among the buttons: the ghost is a 1px underline with
   transparent sides, so a radius has nothing to round but the two ends of that
   rule, which curve up and read as a smile rather than as a button. */
.btn--ghost {
  background: transparent;
  border-color: transparent;
  border-bottom: 1px solid var(--steel);
  padding: 15px 4px;
  font-weight: 400;
  border-radius: 0;
}
.btn--ghost:hover { background: transparent; border-bottom-color: var(--orange); }

.btn--solid { background: var(--navy); border-color: var(--navy); color: #fff; }
.btn--solid:hover { background: var(--orange-deep); border-color: var(--orange-deep); }

/* ---------- services ribbon ---------- */

/* This strip is the only route from the home page to the eight service pages,
   so it is sized as navigation rather than as a caption: --step--1 rather than
   the eyebrow size, and every link carries its own padding so the touch target
   clears the 24px floor in SC 2.5.8. At the eyebrow size the targets were 16px
   tall and 4px apart, which no thumb can hit. */
.ribbon {
  position: relative;
  z-index: 2;
  border-top: 1px solid var(--line-d-firm);
  background: var(--n-900);
  padding: 20px 0 26px;
  font-family: var(--font-mono);
  font-size: var(--step--1);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--mist-4);
}
/* Separated by a gap and not by a typed character. A middot replaced the slash
   this shipped with, because peer services read as a breadcrumb trail with a
   slash between them; the gap is the same reasoning carried one step further.
   Any separator drawn between items sits at the end of a line whenever the next
   item wraps, and with eight items it wrapped on every width measured: four of
   the six lines on a phone closed on a dangling middot. A gap cannot dangle. */
/* A grid rather than a wrapping row, because with no separator between them a
   flex row packs two short names onto one line and they read as one label:
   AI GOVERNANCE and ISO/IEC 42001 AUDIT sat together on a phone. Tracks keep
   one service per cell at every width, and 240px is the longest label
   (AI ENTERPRISE READINESS) with room to breathe: four columns at container
   width, two on a tablet, one on a phone. */
.ribbon ul {
  list-style: none; margin: 0; padding: 0;
  display: grid;
  /* Four, two or one: track counts that divide eight, so the last row is
     always full. auto-fit was laying three tracks in the middle of the range
     and leaving the bottom row one cell short. Four holds to 1023px, where the
     longest label still has room. */
  grid-template-columns: repeat(4, 1fr);
  /* 24 rather than 34, measured against the two widest cells: AI LIFECYCLE
     ENGINEERING at 240px and the flagged ISO/IEC 42001 AUDIT at 233px. A 34px
     gap leaves a 253px track, which both clear by too little to survive a
     metric shift, including the moment before the mono webfont loads. */
  column-gap: 24px;
  row-gap: 6px;
}
/* No rules between the items. The squares already mark where each service
   starts, and a hairline under all eight plus one under the label added three
   more horizontal lines to a band that sits between two of them: the strip
   read as ruled paper rather than as a list. The grid alone carries the
   alignment. */
.ribbon li a { color: inherit; display: flex; align-items: baseline; gap: 12px; padding: 8px 0; }
/* The marker is a square of the signal color, and it sits on the baseline
   rather than centred on the line: a flex item with no text baselines on its
   bottom margin edge, so the square lands exactly where the type does.
   Generated, so it stays out of the link's accessible name. */
/* The square marker becomes a dot. --r-full rather than a step off the ladder:
   at 6px even --r-xs is a third of the box, so the marker is either a circle or
   a square that looks like a circle that failed, and the circle is the honest
   one. It is the only shape on the page that changes kind rather than degree. */
.ribbon li a::before {
  content: "";
  flex: none;
  width: 6px; height: 6px;
  background: var(--orange);
  border-radius: var(--r-full);
}
.ribbon a:hover { color: var(--orange-soft); text-decoration: none; }
.ribbon li a:hover::before { background: var(--orange-soft); }
/* The flag stands in the marker's place rather than trailing the label, so the
   row carries one mark, not two. Real text, not a generated ::after, because
   "New!" is information and has to reach a screen reader with the link.
   --orange-deep rather than the marker's --orange: white sits on it at 4.97:1,
   where the lighter fill would be 3.76:1 and fail the floor at this size. */
.ribbon li.is-new a::before { content: none; }
.ribbon .tag {
  background: var(--orange-deep);
  color: #fff;
  font-size: var(--step--2);
  letter-spacing: 0.1em;
  padding: 2px 6px;
  /* A badge is a pill everywhere else in the ladder's idiom, and this one is
     ~18px tall, so --r-md would already round it most of the way. */
  border-radius: var(--r-full);
}

/* The overview page heads the strip it summarises. Orange and wider tracking
   so it reads as the label of the eight rather than as a ninth service. */
.ribbon-label { margin: 0 0 10px; letter-spacing: 0.16em; }
.ribbon-label a { color: var(--orange-soft); }

/* ---------- sections ---------- */

.section { padding: 64px 0; }
/* A one-line lede does not earn the full 64px band: it reads as a ribbon with
   nothing in it. Half the padding keeps it a band, not a gap. */
.section--slim { padding: 30px 0; }
.section--deep  { background: var(--n-700); color: #fff; }
.section--night { background: var(--n-900); color: #fff; border-top: 1px solid var(--line-d); }
.section--navy  { background: var(--navy); color: #fff; }
.section--mist  { background: var(--paper-2); }
.section--deep h2, .section--night h2, .section--navy h2,
.section--deep h3, .section--night h3, .section--navy h3 { color: #fff; }
/* Underlined, not just recolored: --orange-soft is 1.43:1 against --mist body
   text, so color alone cannot mark a link inside a paragraph (SC 1.4.1). */
.section--deep a:not(.btn), .section--night a:not(.btn), .section--navy a:not(.btn) { color: var(--orange-soft); text-decoration: underline; }

.section-title { font-size: var(--step-3); margin-bottom: 1.1em; }

/* Interior page hero: the home contour field, on every page. Content-driven
   height, deliberately not home's 498px. An interior hero is a kicker, a title
   and one line of subtitle, so forcing it to home's height (home carries a lead
   paragraph and a button row) left ~100px of empty field under the subtitle on
   top of the bottom padding. The two heroes share a rhythm instead of a height:
   the title clears the out-of-flow header by 64px and the field closes 72px
   under the last line. */
.page-hero {
  position: relative;
  z-index: 2;
  padding: calc(var(--header-h) + 64px) 0 72px;
  color: #fff;
}
.page-hero h1 { color: #fff; font-size: var(--step-5); letter-spacing: -0.028em; margin: 0; }
.page-hero .subtitle { margin: 0.6em 0 0; max-width: 52em; font-size: var(--step-1); color: var(--mist); text-wrap: pretty; }

/* Banner used as the page header on interior pages */
.page-banner {
  background: var(--n-800);
  color: #fff;
  padding: 64px 0;
  position: relative;
  overflow: hidden;
}
.page-banner--photo::before {
  content: "";
  position: absolute; inset: 0;
  background: var(--scrim);
}
.page-banner .container { position: relative; }
.page-banner h1 { color: #fff; font-size: var(--step-4); margin: 0; letter-spacing: -0.025em; }
.page-banner .subtitle { margin: 0.6em 0 0; max-width: 52em; font-size: var(--step-1); color: var(--mist); text-wrap: pretty; }

/* ---------- card grids ---------- */

/* Variable-count grids (interior, blog index): real gaps + bordered cards,
   so an uneven last row never shows a ghost cell */
.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
  gap: 24px;
}
.card-grid .card { border: 1px solid var(--line-l); }
.section--deep .card-grid .card, .section--night .card-grid .card,
.section--navy .card-grid .card { border-color: var(--line-d); }
.section--deep .card, .section--night .card { background: var(--n-600); }

.card {
  background: #fff;
  padding: 26px 22px;
  display: flex;
  flex-direction: column;
  border-radius: var(--r-md);
}
/* Service icon: the services overview cards. */
.svc-ico { color: var(--orange); display: block; margin-bottom: 14px; }
.svc-ico svg { width: 34px; height: 34px; display: block; overflow: visible; }
.card h2, .card h3 { font-size: 1.05rem; margin: 0 0 8px; }
.card p { font-size: var(--step--1); line-height: 1.62; margin: 0 0 14px; }
.section--deep .card h3, .section--night .card h3 { color: #fff; }
.section--deep .card p, .section--night .card p { color: var(--mist-2); }
.card .more {
  margin-top: auto;
  font-family: var(--font-mono);
  font-size: var(--step--2);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--orange-deep);
  padding-top: 4px;
}
.section--deep .card .more, .section--night .card .more { color: var(--orange-soft); }
/* One step down from the card holding it: a radius nested inside another
   should always be the smaller of the two, or the inner box reads as bulging
   out of the outer one. The card's own padding keeps the two arcs apart, so
   this is about the picture looking cut to the same pattern, not about the
   concentric-radius arithmetic that applies when a box sits flush inside.

   The picture arms are the same rule reached through the <picture> wrapper. A
   card thumbnail is wrapped, a card thumbnail on a page that still writes its
   own <img> is not, so both paths have to match. Written out rather than
   loosened to `.card img`, which would outrank nothing today and start matching
   images inside card bodies that this max-width was never meant to cap. The
   radius rides on the same declaration block, so a wrapped thumbnail rounds
   exactly like a bare one. */
.card > img, .card > a > img,
.card > picture > img, .card > a > picture > img { width: 100%; max-width: 190px; height: auto; margin: 0 auto 18px; border-radius: var(--r-sm); }

.card-grid--light .card { background: #fff; }

/* Home's "where to start" trio. Its own track size because --card-grid's
   250px auto-fill lays four tracks at container width and leaves the fourth
   standing empty next to three cards; 300px resolves to exactly three, two on
   a tablet and one on a phone. The card is a condition and a destination with
   no paragraph between them, so the condition carries the size a card title
   and a card body would otherwise share. */
.start-grid { grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); }
.start-grid .card h3 { font-size: 1.12rem; line-height: 1.35; margin-bottom: 20px; }

/* Post images are whatever ratio the author had: AUTHORING.md asks for a card
   image and never specifies one, and today's six run from 0.83 to 3.08. Most of
   them are slide exports whose text reaches the edge, so cover was cutting words
   off. contain keeps the fixed box (the grid needs equal card heights) and mats
   the leftover against the card ground instead of cropping the image.

   The picture arms carry the aspect-ratio through the <picture> wrapper. Miss
   one and that card's image reverts to its natural ratio, which is the 3.08
   slide export blowing the row's card heights apart. */
.card--post > a > img, .card--post > img,
.card--media > a > img, .card--media > img,
.card--post > a > picture > img, .card--post > picture > img,
.card--media > a > picture > img, .card--media > picture > img {
  max-width: 100%; width: 100%;
  aspect-ratio: 16 / 10; object-fit: contain;
}

/* ---------- publication list ---------- */

/* Grouped by year rather than run as one flat list, so the run of seventeen
   entries has somewhere for the eye to rest. */
.pub-year {
  font-size: var(--step-1);
  color: #fff;
  margin: 34px 0 0;
  padding-bottom: 8px;
  border-bottom: 1px solid var(--line-d-firm);
}
.pub-list {
  list-style: none;
  margin: 16px 0 0;
  padding: 0;
  display: grid;
  gap: 14px;
}
/* One card per row rather than a grid of them: several titles run past 100
   characters, and a three-across grid breaks them over five lines each. */
.pub {
  display: grid;
  grid-template-columns: 132px minmax(0, 1fr);
  /* The chip is taller than three lines of text, so without a greedy last row
     the spare height is shared out between the title, the authors and the
     venue and the three stop reading as one block. */
  grid-template-rows: auto auto 1fr;
  gap: 4px 22px;
  padding: 20px 22px;
  background: var(--n-600);
  border: 1px solid var(--line-d);
  border-radius: var(--r-md);
}

/* The chip spans all three text rows, so a card is the same height whether its
   title runs one line or three. Papers without artwork fall back to the brand
   mark, which is contained and inset rather than cropped to the frame. */
.pub-thumb {
  grid-row: 1 / span 3;
  align-self: start;
  width: 132px;
  aspect-ratio: 4 / 5;
  height: auto;
  object-fit: cover;
  border: 1px solid var(--line-d-firm);
  border-radius: var(--r-sm);
}
.pub-thumb--mark {
  object-fit: contain;
  padding: 18px;
  background: var(--n-700);
}
.pub-title {
  font-size: 1.05rem;
  line-height: 1.4;
  margin: 0;
  font-weight: 600;
}
/* Matched to the specificity of the blanket `.section--night a:not(.btn)` rule
   above, which would otherwise make all seventeen titles orange and underlined.
   Cards solve the same problem the same way. */
.section--deep .pub-title a, .section--night .pub-title a {
  color: #fff;
  text-decoration: none;
}
.section--deep .pub-title a:hover, .section--night .pub-title a:hover {
  color: var(--orange-soft);
  text-decoration: underline;
}
.pub-authors, .pub-venue {
  font-size: var(--step--1);
  line-height: 1.55;
  margin: 0;
  color: var(--mist-2);
}
.pub-venue { font-family: var(--font-mono); }

/* ---------- insights trio ---------- */

.insights-row {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 30px;
  margin-top: 22px;
}
/* --orange on --n-900 is 5.0:1, which passes, but at the eyebrow size in a
   tracked-out mono uppercase it reads thinner than the number suggests, and
   the kicker is the only thing typing each item in the trio. --orange-soft
   takes it to 10.1:1 on the same ground.

   Size holds the rank: the kicker sits at the 10pt floor, one point under the
   MEDIA header above it, which is bold at 11pt. Both are mono, uppercase and
   orange 20px apart, so with neither weight nor size between them they read as
   two headings of equal rank rather than a header and the item under it. */
.insights-row .k {
  font-family: var(--font-mono);
  font-size: var(--step--2);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--orange-soft);
}
.insights-row h3 { font-size: 1.03rem; margin: 8px 0 6px; line-height: 1.3; }
/* titles stay white; the section-level orange link rule must not win here */
.section--night .insights-row h3 a { color: #fff; }
.section--night .insights-row h3 a:hover { color: #fff; text-decoration: underline; }
.insights-row p { font-size: var(--step--1); margin: 0; line-height: 1.55; color: var(--steel-2); }
/* ---------- blog index: the featured item ---------- */

/* The newest item on the blog index, given the width the grid cannot. Two
   columns rather than the grid card's stacked image, because a 16:10 image at
   full container width is close to 700px tall and pushes everything else off
   the screen; side by side it stays the height of its own text. */
.card--feature {
  display: grid;
  grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr);
  gap: 34px;
  align-items: center;
  padding: 26px;
  margin-bottom: 24px;
  border: 1px solid var(--line-d);
  /* --r-lg, not the grid card's --r-md: it is the widest panel on the page and
     a radius that reads as a corner at 300px reads as a nick at 1100px. */
  border-radius: var(--r-lg);
}
/* The one image on the site that is not lazy: it is the first thing in the
   viewport on this page, so deferring it is deferring the largest paint.
   resources/blog/index.html writes this <img> inline rather than through an
   include, so it is bare today; the picture arm is here so that whoever moves
   that markup onto a wrapper does not also silently drop the 16:10 box and
   push the rest of the index off the screen. */
.card--feature > a > img,
.card--feature > a > picture > img {
  width: 100%;
  aspect-ratio: 16 / 10;
  object-fit: contain;
  margin: 0;
  max-width: none;
  border-radius: var(--r-md);
}
.feature-txt {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}
/* Roughly halfway between the grid card's 1.05rem and the page h1: enough to
   read as the lead without competing with the hero above it. */
.card--feature h2 {
  font-size: var(--step-2);
  line-height: 1.18;
  margin: 10px 0 12px;
  text-wrap: balance;
}
.card--feature p { margin: 0 0 18px; }
/* .card .more uses margin-top:auto to sit at the foot of a flex column card.
   Here the column is centred against a tall image, so auto would push the link
   away from the paragraph it belongs to. */
.card--feature .more { margin-top: 0; }

/* One column below the point where the text column gets too narrow to hold a
   title on two lines. */
@media (max-width: 760px) {
  .card--feature {
    grid-template-columns: 1fr;
    gap: 20px;
  }
}

/* ---------- home media rail ---------- */

/* The Media section on the home page: a horizontal snap rail of recent work,
   built by _includes/media-carousel.html. It carries .card--media so a card
   here is the same object as the same item on the Media pages, which is why
   there are no card rules below: only the track, and the arrows under it.

   The track scrolls natively. scroll-snap plus overflow-x is the whole
   mechanism, so a swipe, a trackpad, and the arrow keys on the focused
   container all work with no script. main.js only reveals the two buttons. */
.media-rail {
  display: flex;
  /* Tighter than the 24px of .card-grid. The cards off to the side are scaled
     down and transform does not reflow, so each one leaves half its lost width
     as empty space on either side: at 0.82 that is 27px a side on top of the
     gap itself. The gap has to come in to keep the row reading as a row.
     assets/js/main.js measures this value rather than repeating it. */
  gap: 16px;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  /* Room for the scrollbar to sit under the cards rather than across them. */
  padding-bottom: 14px;
  margin-top: 22px;
}
/* 300px against the 250px of .card-grid. A rail card is read one at a time
   rather than scanned in a row, and the centre one is the size the eye lands
   on, so it carries the longest titles here (the Quantization Tier article,
   the MILCOM workshop) without running to five lines. */
.media-rail .card {
  flex: 0 0 300px;
  scroll-snap-align: center;
  border: 1px solid var(--line-d);
  /* Scaled from the middle, so a card off to the side shrinks toward its own
     centre line and sits visually inset against the taller one beside it. */
  transform-origin: center center;
}

/* Everything below applies only once main.js has taken the rail over. Without
   it the rail is a plain snapping scroller: every card full size, a visible
   scrollbar, and no drift. The class is the switch between the two, so the
   no-script state is never styled for behaviour it does not have. */
.media-rail.is-live {
  scroll-snap-type: none;
  cursor: grab;
  /* The scrollbar goes because the rail now moves on its own and is dragged
     rather than scrolled: a bar tracking a position nobody set reads as a
     progress indicator that will not sit still. Scrollability is untouched. */
  scrollbar-width: none;
}
.media-rail.is-live::-webkit-scrollbar { display: none; }
.media-rail.is-dragging {
  cursor: grabbing;
  /* Without this a pull across the cards selects their headlines instead. */
  user-select: none;
}
/* The size and lift are written per frame from the card's distance to the
   middle of the track, so there is no transition here: one would fight the
   frame loop and smear the scaling behind the motion. */
.media-rail.is-live .card { will-change: transform; }
/* The track is focusable so the arrow keys reach it, so it needs a focus ring
   of its own: without this the only feedback for tabbing onto the rail is the
   first card's link ring, which is inside the box that actually has focus. */
.media-rail:focus-visible {
  outline: 2px solid var(--orange-soft);
  outline-offset: 4px;
}
.rail-ctl {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: 18px;
}
/* The link out is the only thing on this row now that the arrows are gone, so
   it sits at the right end of it rather than under the middle of the track. */
.rail-ctl .rowmore { margin: 0 0 0 auto; }

/* The trio is a sample, not the archive: the link out sits with it. */
.rowmore {
  font-family: var(--font-mono);
  font-size: var(--step--2);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  margin: 26px 0 0;
}

/* ---------- leadership band + footer ---------- */

.band {
  background: var(--n-900);
  color: #fff;
  padding: 52px 0;
  border-top: 1px solid var(--line-d);
}
.band .container { display: flex; align-items: center; gap: 32px; flex-wrap: wrap; }
.band a.biolink { color: inherit; display: flex; gap: 13px; align-items: center; }
.band a.biolink:hover { text-decoration: none; }
.band a.biolink:hover .nm { color: var(--orange-soft); }
.band img.face { width: 52px; height: 52px; border-radius: 50%; object-fit: cover; }
.band .nm { font-weight: 700; font-size: 0.9rem; }
.band .rl { font-family: var(--font-mono); font-size: var(--step--2); color: var(--steel); letter-spacing: 0.08em; text-transform: uppercase; }
.band .close { margin-left: auto; text-align: right; }
.band .close p { margin: 0 0 12px; font-family: var(--font-display); font-size: 1.2rem; }

.site-footer {
  background: var(--n-900);
  color: #fff;
  border-top: 1px solid var(--line-d);
}
/* Padding on the links rather than on the row: these are the last targets on
   the page and often the ones reached on a phone, so each one carries its own
   24px-plus hit area (SC 2.5.8) instead of sitting as bare text on a line. */
.site-footer .footer-nav {
  display: flex; justify-content: center; flex-wrap: wrap;
  column-gap: 34px;
  padding: 12px 0 6px;
  border-bottom: 1px solid var(--line-d-soft);
  font-family: var(--font-mono);
  font-size: var(--step--2); letter-spacing: 0.12em; text-transform: uppercase;
}
.site-footer .footer-nav a { color: var(--mist-3); display: inline-block; padding: 9px 0; }
.site-footer .footer-nav a:hover { color: var(--orange-soft); text-decoration: none; }
.site-footer .cert-row {
  display: flex; align-items: center; justify-content: center; gap: 28px; flex-wrap: wrap;
  padding: 30px 0 12px;
}
/* the two marks are one home link, so the gap between them moves off .cert-row
   and onto the link itself to keep the row's spacing identical. max-width has
   to be released: the global 100% would resolve against the link's own
   shrink-to-fit width and collapse both images to zero. */
.site-footer .footer-home { display: inline-flex; align-items: center; gap: 28px; }
.site-footer .footer-home:hover { text-decoration: none; }
.site-footer .round-logo { height: 44px; width: auto; max-width: none; opacity: 0.9; }
.site-footer .lock { height: 44px; width: auto; max-width: none; opacity: 0.9; }
.site-footer .footer-home:hover .round-logo,
.site-footer .footer-home:hover .lock { opacity: 1; }
.site-footer .cnote {
  font-family: var(--font-mono);
  font-size: var(--step--2); letter-spacing: 0.09em; text-transform: uppercase;
  color: var(--steel); line-height: 1.6; margin: 0; text-align: center;
}
.site-footer .badges { display: flex; align-items: center; gap: 16px; }
.site-footer .badges img { height: 54px; width: auto; }
.site-footer .copyright {
  text-align: center;
  padding: 10px 0 22px;
  font-family: var(--font-mono);
  font-size: var(--step--2); letter-spacing: 0.06em;
  color: var(--steel);
}

/* ---------- interior content components (v1 classes, restyled) ---------- */

.media-row { display: grid; grid-template-columns: 1fr 1fr; gap: 48px; align-items: center; }
.media-row + .media-row { margin-top: 56px; }
.media-row--flip > .media-row-img { order: 2; }

.prose { max-width: 760px; margin-inline: auto; counter-reset: slide; }
/* An article body centres in the container; a lede on a non-article page has to
   line up with the hero title and the .seclbl above the content instead. */
.prose--flush { margin-inline: 0; }
.prose img { margin: 28px auto; max-width: 100%; height: auto; border-radius: var(--r-md); }
.prose ul, .prose ol { margin: 0 0 1em; padding-left: 1.4em; }
.prose li { margin-bottom: 0.4em; }
/* The pull-quote, the service cross-reference and the offering spine are all
   the same object: a coloured rule with no box around it, and all three draw
   that rule as a filled bar rather than as a border.

   A border cannot have a rounded cap. border-radius on a box whose only border
   is one edge tapers that edge toward its zero-width neighbour, which thins
   the rule as it curves and reads as a hook at small radii and as a bracket at
   large ones. A bar is a box, so it takes --r-full and gets a real cap at full
   thickness. The 3px the border used to occupy moves into the padding, so the
   text sits exactly where it did. */
.prose blockquote {
  margin: 1.6em 0; padding: 4px 0 4px 25px;
  position: relative;
  color: var(--navy); font-size: var(--step-1);
}
.prose blockquote::before {
  content: "";
  position: absolute; left: 0; top: 0; bottom: 0;
  width: 3px;
  background: var(--orange);
  border-radius: var(--r-full);
}
.prose .article-meta { color: var(--orange-deep); font-family: var(--font-mono); font-size: var(--step--1); margin-bottom: 2em; }
/* An image with a caption is a figure. .prose img already centers itself and
   carries the vertical rhythm, so the figure only has to stop margins from
   doubling up. */
/* A flat icon used as an accent, not an illustration. Without a cap it renders
   at the full 760px column, which is what a 1200x1200 clipboard glyph was doing
   in the AI Safety Scores post. */
.prose img.mark { width: 140px; margin: 26px auto; }
.prose figure { margin: 28px 0; }
.prose figure img { margin: 0 auto; }
.prose figcaption {
  margin-top: 12px;
  font-size: var(--step--1); line-height: 1.55;
  color: var(--ink-2);
}
/* inline accent inside a paragraph: lead-in labels ("The Goal:", "What it
   catches:") and the occasional brand mention. Body size, so it takes the
   readable tokens, never --orange. Posts render on section--deep, so the
   on-dark override below is the one that actually applies. */
.prose .accent { color: var(--orange-deep); }

.section--deep .prose, .section--night .prose, .section--navy .prose { color: var(--mist); }
.section--deep .prose h2, .section--deep .prose h3,
.section--night .prose h2, .section--night .prose h3,
.section--navy .prose h2, .section--navy .prose h3 { color: #fff; }
.section--deep .prose blockquote, .section--night .prose blockquote,
.section--navy .prose blockquote { color: var(--mist); }
.section--deep .prose .article-meta, .section--night .prose .article-meta { color: var(--orange-soft); }
.section--deep .prose .accent, .section--night .prose .accent,
.section--navy .prose .accent { color: var(--orange-soft); }
.section--deep .prose figcaption, .section--night .prose figcaption,
.section--navy .prose figcaption { color: var(--steel-2); }
/* Body links on a dark ground. The global rule paints links --orange-deep with
   no underline, which is 3.29:1 on --n-700 and invisible mid-sentence: the
   migrated posts papered over it by wrapping every link in a <u>. --orange-soft
   is the on-dark voice of the same color at 8.25:1, and the underline is how a
   link is identifiable without relying on color at all. */
.section--deep .prose a, .section--night .prose a, .section--navy .prose a {
  color: var(--orange-soft);
  text-decoration: underline;
  text-underline-offset: 2px;
}
.section--deep .prose a:hover, .section--night .prose a:hover,
.section--navy .prose a:hover { text-decoration-thickness: 2px; }

/* ---------- blog content components ---------- */

/* Two block components for post bodies. Both are a wrapper div carrying
   markdown="1", not a Liquid include: an include takes string attributes and
   cannot take a Markdown body, so a slide's bullets or a callout's prose would
   have to be pipe-delimited into one attribute. kramdown parses Markdown inside
   these divs, so the source stays prose. Only figure.html is an include, being
   four attributes and no body. See AUTHORING.md.

   Anatomy follows .featured, the house panel-on-dark: --n-600 ground, hairline
   border. Deliberately the quiet variant, never .featured's orange border,
   because these repeat down a page and only one card should read as a headline. */

.slides { display: grid; gap: 22px; margin: 26px 0 34px; }
.slide {
  background: var(--n-600);
  border: 1px solid var(--line-d);
  border-top: 2px solid var(--orange);
  padding: 22px 26px 24px;
  /* The 2px orange top and the 1px hairline sides meet inside the arc, so the
     orange tapers into the hairline through each top corner instead of ending
     on a butt joint. That taper is the reason the rule reads as a cap here
     rather than as a stripe that stops short. */
  border-radius: var(--r-md);
}
/* the card supplies the padding, so the first and last child must not add to it */
.slide > :first-child { margin-top: 0; }
.slide > :last-child { margin-bottom: 0; }
.prose .slide h2, .prose .slide h3,
.prose .callout h2, .prose .callout h3 { font-size: var(--step-1); margin: 0 0 12px; }
/* Numbered only inside .slides, so a lone .slide stays unnumbered. Generated,
   so the number always matches what is on the page. The counter resets on
   .prose rather than .slides so a deck can be interrupted by a pull-quote and
   resume at the right number instead of restarting at one. */
.slides > .slide { counter-increment: slide; }
.slides > .slide::before {
  content: "Slide " counter(slide);
  display: block;
  font-family: var(--font-mono);
  font-size: var(--step--2); letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--orange-soft);
  margin-bottom: 10px;
}
/* The deck exported at anything from 706px to 1000px wide, so on the bare
   `.prose img` rule the slides rendered at three different widths down one
   page. Filling the card is what makes them one size again. */
.prose .slide img { width: 100%; margin: 18px 0 0; border-radius: var(--r-sm); }
/* A caption-less figure.html used to arrive here as <p><img></p>, because <img>
   is a span-level element to kramdown and a lone one on its own line becomes a
   paragraph. <picture> is block-level to kramdown, so that wrapper is gone, and
   with it the paragraph's 1em bottom margin: the one slide with a line of text
   under its image (Governance is not MLOps, in the BSides 2026 post) closed the
   gap to nothing. The image carries it now. The last-child arm is what
   `.slide > :last-child` used to do and can no longer reach, since that
   selector now stops at the <picture>. */
.prose .slide > picture > img { margin-bottom: 1em; }
.prose .slide > picture:last-child > img { margin-bottom: 0; }

/* A labelled aside: a definition or a warning. Distinct from .prose blockquote,
   which is the unlabelled pull-quote and sits on the section ground with only a
   rule; this one is a filled box with a mono label. */
.callout {
  background: var(--n-600);
  border-left: 3px solid var(--orange);
  padding: 18px 22px;
  margin: 26px 0;
  border-radius: var(--r-md);
}
.callout > :first-child { margin-top: 0; }
.callout > :last-child { margin-bottom: 0; }
.callout .lbl {
  display: block;
  font-family: var(--font-mono);
  font-size: var(--step--2); letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--orange-soft);
  margin-bottom: 8px;
}

.section--deep h2, .section--night h2 { color: #fff; }
.section--deep .card h2, .section--night .card h2,
.section--deep .card h2 a, .section--night .card h2 a { color: #fff; }
.section--deep .profile-header .role, .section--night .profile-header .role { color: var(--orange-soft); }

/* ---------- blog author ribbon ---------- */

/* Both ends of a post, from one include: a compact byline above the article and
   a bio card below it. See _includes/author-ribbon.html and _data/authors.yml.
   Both variants lay out one author or two, and the two-author case is a wrap
   rather than a second column, so it holds at any width.

   The ribbon sits inside .prose, so it inherits the underlined on-dark link
   above. That is right for a link inside a sentence and wrong for a name or a
   photo, which read as links from position alone: both variants turn it off and
   put it back on hover. The selector has to carry all three grounds to outweigh
   `.section--deep .prose a`, which a bare `.author-ribbon a` loses to. */
.section--deep .prose .author-ribbon a, .section--night .prose .author-ribbon a,
.section--navy .prose .author-ribbon a { text-decoration: none; }
.section--deep .prose .author-ribbon a:hover, .section--night .prose .author-ribbon a:hover,
.section--navy .prose .author-ribbon a:hover { text-decoration: underline; }
.author-ribbon .author-role {
  display: block;
  font-family: var(--font-mono);
  font-size: var(--step--2); letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--orange-soft);
}

.author-ribbon--byline {
  display: flex; flex-wrap: wrap; gap: 14px 34px; align-items: center;
  margin-bottom: 2.6em; padding-bottom: 22px;
  border-bottom: 1px solid var(--line-d);
}
.author-chip { display: flex; gap: 14px; align-items: center; color: inherit; }
/* Both ribbon images are a fixed square crop, so they have to beat `.prose img`,
   which sets `height: auto` (the source portrait would stretch the circle into
   an oval) and a 28px vertical rhythm margin (which would push the image out of
   line with the name beside it). Two classes outweigh one class plus a tag. */
.prose .author-avatar {
  margin: 0;
  flex: none; /* a flex item, so without this it shrinks out of round on phones */
  width: 46px; height: 46px;
  border-radius: 50%;
  object-fit: cover; object-position: 50% 18%;
  border: 1px solid var(--line-d);
}
.author-ribbon--byline .author-name {
  display: block;
  font-family: var(--font-display); font-weight: 700;
  font-size: var(--step-0); letter-spacing: -0.015em; line-height: 1.3;
  color: #fff;
}
.author-chip:hover .author-name { color: var(--orange-soft); }

.author-ribbon--bio {
  margin-top: 3.4em; padding-top: 26px;
  border-top: 1px solid var(--line-d);
}
.author-card { display: grid; grid-template-columns: 104px 1fr; gap: 24px; align-items: start; }
.author-card + .author-card { margin-top: 30px; }
/* A circle, matching .author-avatar at the other end of the same post. This
   was a rounded square first, on the argument that matching the avatar is a
   crop decision rather than a corner one. Rendered side by side that does not
   survive: at the portrait step a 104px square reads as an app icon, and it
   left one person wearing two shapes on one page. The object-position crop is
   already centred on the face, so nothing is lost to the circle. */
.prose .author-photo {
  margin: 0;
  width: 104px; height: 104px;
  object-fit: cover; object-position: 50% 18%;
  border: 1px solid var(--line-d);
  border-radius: 50%;
}
.author-ribbon--bio .author-name {
  margin: 0;
  font-family: var(--font-display); font-weight: 700;
  font-size: var(--step-1); letter-spacing: -0.02em; line-height: 1.2;
}
.author-ribbon--bio .author-name a { color: #fff; }
.author-ribbon--bio .author-name a:hover { color: var(--orange-soft); }
.author-ribbon--bio .author-role { margin: 6px 0 12px; }
.author-bio { margin: 0; color: var(--mist-2); font-size: var(--step--1); }
.author-more {
  margin: 12px 0 0;
  font-family: var(--font-mono);
  font-size: var(--step--2); letter-spacing: 0.1em; text-transform: uppercase;
}

.badge-row { display: flex; justify-content: center; align-items: center; gap: 56px; flex-wrap: wrap; }
.badge-row img { max-width: min(380px, 100%); height: auto; }

/* A filled 2px box rather than a border, so unlike the rules above this one
   really does take round caps. */
.rule-soft { border: 0; height: 2px; margin: 8px 0; background: var(--line-d); border-radius: var(--r-full); }

/* Standalone images in flowed sections keep a sane width (icons ship at 1280px).
   The picture arms are the same three positions reached through the <picture>
   wrapper: without them a 1280px icon renders at 1280px across the container. */
.section > .container > img,
.section > .container > a > img,
.section > .container > p > img,
.section > .container > picture > img,
.section > .container > a > picture > img,
.section > .container > p > picture > img {
  max-width: 460px;
  margin-inline: auto;
  border-radius: var(--r-md);
}

.profile-header { display: grid; grid-template-columns: 280px 1fr; gap: 48px; align-items: start; }
.profile-header img { width: 100%; border-radius: var(--r-lg); }
.profile-header .role {
  color: var(--orange-deep); font-family: var(--font-mono); font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.08em; font-size: var(--step--2); margin-bottom: 0.4em;
}

/* overflow:hidden is what actually rounds this one: the radius is on the
   wrapper and the corners belong to the iframe inside it, which is a replaced
   element painting its own square. */
.embed { position: relative; aspect-ratio: 16 / 9; margin: 28px 0; border-radius: var(--r-md); overflow: hidden; }
.embed iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }

.form-embed { display: block; width: 100%; min-height: 1200px; border: 0; margin: 24px 0; background: #fff; border-radius: var(--r-md); }

.pdf-embed { display: block; width: 100%; height: 80vh; min-height: 520px; border: 1px solid var(--line-l); margin: 24px 0; border-radius: var(--r-md); }


/* ---------- service-page components (ground-up pages) ---------- */

.page-hero .kicker {
  font-family: var(--font-mono);
  font-size: var(--step--2);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--orange);
  margin: 0 0 10px;
}

.featured {
  background: var(--n-600);
  border: 1px solid var(--orange-line);
  padding: 30px 34px;
  margin-bottom: 8px;
  border-radius: var(--r-lg);
}
.featured h2 { font-size: 1.5rem; letter-spacing: -0.02em; margin: 0 0 6px; color: #fff; }
/* Service offerings on one page are peers, and they render as peers. There used
   to be a .featured--quiet variant here that gave every offering after the
   first a hairline instead of the orange border and a darker ground, so only
   one card per page read as the headline. It made the second and third
   offerings look like footnotes to the first, which is what issues #141, #142
   and #144 all reported; retired 2026-07-31 after dot-voting picked the
   all-equal frame. The margin the variant carried moves here, where it belongs:
   it was spacing between cards, not part of being quiet. */
.featured + .featured { margin-top: 26px; }
.credline {
  font-family: var(--font-mono);
  font-size: var(--step--2); letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--orange-soft); margin: 0 0 16px;
}
/* The measure is 74ch, not the 68ch it was. A ch is the width of '0', which in
   Public Sans is wider than average lowercase text, so 68ch held only ~91
   characters: enough that a one-sentence intro overflowed by a few pixels and
   dropped its last word alone onto a second line, 500px short of the panel
   edge. 74ch clears every single-sentence intro on the service pages, and
   widens the multi-line ones by 5%, which is not a visible change.
   text-wrap: pretty backs it up on the intros that do still wrap, and on the
   other prose runs below. Unsupported browsers ignore it and wrap as before. */
.featured .intro { color: var(--mist); font-size: 0.91rem; line-height: 1.66; margin: 0 0 4px; max-width: 74ch; text-wrap: pretty; }

.steps { display: grid; grid-template-columns: repeat(3, 1fr); gap: 22px; margin: 20px 0 4px; }
/* Four steps read as 2x2 rather than a row of three with an orphan. */
.steps--quad { grid-template-columns: repeat(2, 1fr); }
.step .ico { color: var(--orange); display: block; margin-bottom: 11px; }
.step .ico svg { width: 30px; height: 30px; display: block; overflow: visible; }
.step h3 { font-size: 0.97rem; margin: 0 0 6px; color: #fff; }
.step p { font-size: var(--step--1); line-height: 1.6; color: var(--mist-2); margin: 0; text-wrap: pretty; }

.proc {
  display: flex; gap: 10px; align-items: center; flex-wrap: wrap;
  margin-top: 20px; padding-top: 16px;
  border-top: 1px solid var(--line-d);
  font-family: var(--font-mono);
  font-size: var(--step--2); letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--mist);
}
.proc .a { color: var(--orange-soft); }
.proc .ext { color: var(--steel); }
.indep { font-size: var(--step--1); color: var(--steel); margin: 14px 0 0; max-width: 70ch; }

/* lifecycle diagram (AI Secure by Design): brand graphic replacing the text ribbon.
   Two art files swap by width: horizontal on wide screens, vertical on narrow,
   so the fine labels stay legible instead of shrinking or needing a swipe. */
.lifecycle-fig { margin: 22px 0 0; padding-top: 16px; border-top: 1px solid var(--line-d); }
.lifecycle-fig .seclbl { margin: 0 0 12px; }
.lifecycle-fig svg { display: block; width: 100%; height: auto; }
.lifecycle-v { display: none; }
/* horizontal pill labels fall below ~14px once the container is under ~820px */
@media (max-width: 820px) {
  .lifecycle-h { display: none; }
  .lifecycle-v { display: block; text-align: center; }
  /* 570 keeps the art at the same drawn scale as the old 520 cap: the vertical
     viewBox widened from 832 to 912 units when the labels moved to their own column. */
  .lifecycle-v svg { max-width: 570px; margin: 0 auto; }
}

/* ATLAS technique map: red teaming page */
.atlas { margin-top: 22px; padding-top: 18px; border-top: 1px solid var(--line-d); }
.atlas .seclbl { margin-bottom: 12px; }
.atlas-row {
  display: grid;
  grid-template-columns: 118px minmax(0, 21ch) 1fr;
  gap: 8px 20px;
  align-items: baseline;
  padding: 9px 0;
  border-bottom: 1px solid var(--line-d-soft);
}
.atlas-row:last-child { border-bottom: 0; padding-bottom: 0; }
.atlas .t-id { font-family: var(--font-mono); font-size: var(--step--2); color: var(--orange-soft); }
.atlas .t-name { font-weight: 700; font-size: var(--step--1); color: #fff; }
.atlas .t-what { font-size: var(--step--1); line-height: 1.55; color: var(--steel-2); text-wrap: pretty; }
/* same rhythm without the ID column, for lists that have no real identifiers */
.atlas--plain .atlas-row { grid-template-columns: minmax(0, 24ch) 1fr; }

/* session catalogue: titles run long, so they get their own line */
.sessions { margin-top: 22px; padding-top: 18px; border-top: 1px solid var(--line-d); }
.sessions .s { padding: 11px 0; border-bottom: 1px solid var(--line-d-soft); }
.sessions .s:last-child { border-bottom: 0; padding-bottom: 0; }
.sessions h3 { font-size: 0.9rem; margin: 0 0 4px; color: #fff; }
.sessions p { margin: 0; font-size: var(--step--1); line-height: 1.55; color: var(--steel-2); max-width: 82ch; text-wrap: pretty; }

.offer { display: grid; grid-template-columns: 52px 1.1fr 1.3fr; gap: 26px; padding: 26px 0; border-top: 1px solid var(--line-d); }
/* The third of the three bars. See .prose blockquote. */
.o-rule { width: 2px; align-self: stretch; min-height: 54px; background: var(--orange); border-radius: var(--r-full); }
.o-main h3 { font-size: 1.19rem; margin: 0 0 8px; color: #fff; }
.o-main h3 a { color: inherit; text-decoration: none; }
.o-main h3 a:hover { color: var(--orange-soft); }
.o-main p { color: var(--mist-2); font-size: var(--step--1); line-height: 1.6; margin: 0; }
.o-main .go {
  display: inline-block; margin-top: 10px;
  font-family: var(--font-mono); font-size: var(--step--2); letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--orange-soft); text-decoration: none;
}
.o-main .go:hover { text-decoration: underline; }

/* Services lead paragraph. Named for the services overview, where it used to
   sit above the card grid; it now opens the home "what we do" section. */
.svc-lead {
  color: var(--mist); font-size: 0.98rem; line-height: 1.7;
  max-width: 74ch; margin: 0 0 8px;
}
.svc-lead strong { color: #fff; font-weight: 600; }
/* A cross-reference to a peer service, sitting between the hero and the offering
   card on the three service pages that have a near neighbour. It opens the
   section, so it inherited the section's full 64px top padding and sat in an
   empty field taller than the note itself: one sentence reading as a band. The
   rule below halves that, and the note keeps a matching gap under it, so it
   reads as a note between two things rather than as a section of its own. */
/* 74ch is the prose measure, inherited from .intro, and it was wrong here: all
   three of these notes are a single sentence of 108 to 121 characters, so 74ch
   broke every one of them onto a second line inside a container half again as
   wide. A measure only matters to a run that wraps; capped at 100ch none of
   the three does at container width, and the cap still catches a longer note
   before it runs the full 1160px. */
/* Same bar as the pull-quote, one pixel thinner. See .prose blockquote. */
.svc-note {
  color: var(--steel); font-size: var(--step--1); line-height: 1.6;
  max-width: 100ch; margin: 0 0 30px; padding-left: 16px;
  position: relative;
}
.svc-note::before {
  content: "";
  position: absolute; left: 0; top: 0; bottom: 0;
  width: 2px;
  background: var(--orange);
  border-radius: var(--r-full);
}
.section:has(> .container > .svc-note:first-child) { padding-top: 30px; }
.svc-note a { color: var(--orange-soft); }

/* services overview: icon card grid */
/* The grid rounds, the cards inside it do not. The 1px gap is not a gap, it is
   the grid's own background showing through as the rules between eight tiles,
   so rounding each tile would open a lens-shaped hole at every junction and
   leave the wall looking chewed. Clipping the whole wall to one radius is the
   only version of this that keeps the rules meeting square where they cross.
   overflow:hidden is doing the work; the corner cards have no radius of their
   own to lose if the grid ever stops clipping. */
.svc-cards {
  display: grid; grid-template-columns: repeat(4, 1fr);
  gap: 1px; background: var(--line-d); border: 1px solid var(--line-d);
  margin-top: 26px;
  border-radius: var(--r-lg);
  overflow: hidden;
}
.svc-card { background: var(--n-600); padding: 26px 22px; display: flex; flex-direction: column; }
/* h2: the service cards are the top-level divisions under the page h1, so the
   outline would otherwise skip a level. h3 stays matched for any card that
   sits under a section heading. */
.svc-card h2, .svc-card h3 { font-size: 1.02rem; margin: 0 0 8px; color: #fff; }
.svc-card h2 a, .svc-card h3 a { color: inherit; text-decoration: none; }
.svc-card h2 a:hover, .svc-card h3 a:hover { color: var(--orange-soft); }
.svc-card > p { font-size: var(--step--1); line-height: 1.58; color: var(--mist-2); margin: 0 0 14px; }
.svc-card .go {
  margin-top: auto;
  font-family: var(--font-mono); font-size: var(--step--2); letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--orange-soft); text-decoration: none;
}
.svc-card .go:hover { text-decoration: underline; }
/* The card count is not a multiple of the column count, so the last row would
   leave the grid's 1px line background showing as empty cells. Stretch the
   final card across whatever the row has left. Each fill rule is scoped to the
   breakpoint whose column count it was derived from: an unscoped span outlives
   the column count it assumes and invents implicit columns below it, which is
   what put a two-column grid on a phone. */
@media (min-width: 1024px) {
  .svc-card:last-child:nth-child(4n + 1) { grid-column: span 4; }
  .svc-card:last-child:nth-child(4n + 2) { grid-column: span 3; }
  .svc-card:last-child:nth-child(4n + 3) { grid-column: span 2; }
}
@media (min-width: 641px) and (max-width: 1023px) {
  .svc-cards { grid-template-columns: repeat(2, 1fr); }
  .svc-card:last-child:nth-child(2n + 1) { grid-column: span 2; }
}
/* One column: every card already fills its row, so there is nothing to span. */
@media (max-width: 640px) {
  .svc-cards { grid-template-columns: 1fr; }
}
.o-ledger { margin: 0; }
.o-ledger dt { font-weight: 700; font-size: var(--step--1); color: #fff; padding-top: 10px; }
.o-ledger dt:first-child { padding-top: 0; }
.o-ledger dd { margin: 2px 0 8px; color: var(--steel-2); font-size: var(--step--1); line-height: 1.55; border-bottom: 1px solid var(--line-d-soft); padding-bottom: 10px; }
.o-ledger dd:last-child { border-bottom: 0; }

.deliv { display: flex; gap: 22px; align-items: center; margin-top: 22px; padding-top: 18px; border-top: 1px solid var(--line-d); }
.deliv img { width: 200px; height: auto; flex: none; border: 1px solid var(--line-d-firm); border-radius: var(--r-sm); }
.deliv h3 { font-size: 1rem; margin: 0 0 6px; color: #fff; }
.deliv p { font-size: var(--step--1); line-height: 1.6; color: var(--mist-2); margin: 0; max-width: 56ch; }

.whenstrip { display: flex; gap: 16px; align-items: baseline; flex-wrap: wrap; padding: 18px 0 4px; border-top: 1px solid var(--line-d); margin-top: 8px; font-size: var(--step--1); color: var(--mist); }
.whenstrip .seclbl { margin: 0; }

.cta-band { background: var(--n-900); color: #fff; border-top: 1px solid var(--line-d); }
/* the line and the button are one thought, so they sit together and centre in
   the band rather than being pushed to its opposite edges */
.cta-band .container { display: flex; align-items: center; justify-content: center; flex-wrap: wrap; gap: 18px 28px; padding-top: 26px; padding-bottom: 26px; }
.cta-band p { margin: 0; font-family: var(--font-display); font-size: 1.06rem; }


/* ---------- About page ---------- */

.founders { display: grid; grid-template-columns: 1fr 1fr; gap: 28px 48px; }
/* Cap the portrait rather than the card: a fluid half-container photo renders
   ~540x680 at desktop, taller than the viewport. The card stays full width so
   the bio keeps a readable measure, and the name plate still anchors to the
   photo because it is positioned against this box. */
/* overflow:hidden was already here to keep the name plate inside the box, and
   it now also clips the portrait and the bottom of the scrim gradient into the
   arc, so the plate's own corners follow the photo's without being restated. */
.fphoto { position: relative; overflow: hidden; max-width: 380px; border-radius: var(--r-lg); }
.fphoto img { width: 100%; aspect-ratio: 4 / 5; object-fit: cover; object-position: top; display: block; }
.fname {
  position: absolute; left: 0; right: 0; bottom: 0;
  padding: 44px 18px 14px;
  background: linear-gradient(180deg, var(--scrim-0) 0%, var(--scrim-deep) 70%);
}
/* Display leading, not the 1.65 body leading: at body leading the name's line
   box is 33px tall for a 20px face, and since the plate is bottom-anchored all
   that slack pushes the name up off the role and onto the subject's chin. */
.fname b { display: block; font-family: var(--font-display); font-size: 1.25rem; line-height: 1.2; letter-spacing: -0.015em; color: #fff; }
.fname span { font-family: var(--font-mono); font-size: var(--step--2); letter-spacing: 0.1em; text-transform: uppercase; color: var(--orange-soft); }
.fbio { color: var(--mist); font-size: var(--step--1); line-height: 1.66; margin: 16px 0 10px; }
.fmeta { font-family: var(--font-mono); font-size: var(--step--2); letter-spacing: 0.08em; text-transform: uppercase; color: var(--steel); margin-bottom: 10px; }
.fcard .more { font-family: var(--font-mono); font-size: var(--step--2); letter-spacing: 0.1em; text-transform: uppercase; color: var(--orange-soft); }

.statband {
  display: grid; grid-template-columns: repeat(3, 1fr);
  background: var(--n-900); color: #fff;
  border-top: 1px solid var(--line-d); border-bottom: 1px solid var(--line-d);
}
.statband .stat { padding: 26px 34px; border-right: 1px solid var(--line-d-soft); }
.statband .stat:last-child { border-right: 0; }
/* Tabular figures so the counting animation does not jitter its own width as
   the digits cycle. Harmless on the cell that carries letters instead. */
.statband .sn { font-family: var(--font-display); font-size: 1.9rem; font-weight: 800; letter-spacing: -0.02em; font-variant-numeric: tabular-nums; }
/* The publication figure is a floor rather than an exact count, so it carries a
   lead-in. Body face at small size, so the number stays what the eye lands on
   and the counting animation still runs on its own line. */
.statband .sn-lead {
  display: block;
  font-family: var(--font-body);
  font-size: var(--step--1);
  font-weight: 400;
  letter-spacing: 0;
  color: var(--mist);
}
.statband .st { font-size: var(--step--1); color: var(--mist); margin-top: 4px; }
.statband .ss { font-family: var(--font-mono); font-size: var(--step--2); letter-spacing: 0.12em; text-transform: uppercase; color: var(--steel); margin-top: 6px; }

/* A cell that carries a sentence rather than a figure: display face at
   statement scale, and a sentence-case source (uppercasing a sentence
   with a period reads as shouting). */
.statband .stat--statement { display: flex; flex-direction: column; justify-content: center; }
.statband .sq {
  font-family: var(--font-display);
  font-size: 1.12rem; font-weight: 700; line-height: 1.28;
  letter-spacing: -0.015em; color: #fff; margin: 0; max-width: 22ch;
}
.statband .stat--statement .ss { text-transform: none; letter-spacing: 0.02em; font-size: var(--step--1); margin-top: 10px; }

/* "The company" ribbon: Meaty flanks the copy on his left, and the two centre
   as one group, requested 2026-07-30. It reads as a unit sitting over the
   centred CTA band and footer below it.

   This is a deliberate reversal of b543031, which stacked Meaty above the copy
   so THE COMPANY and the paragraph could start at the container edge. Neither
   flanking nor centring can hold that: the copy now sits wherever the group
   lands, so it no longer lines up with 23, SDVOSB and the section labels above
   and below. That is the known cost of this layout, and it is the choice here,
   not a regression to fix.

   flex-grow has to stay off .company-copy. With it on, the copy absorbs the
   free space, the group spans the full container and justify-content has
   nothing left to centre into.

   Phones keep the stack: see the max-width 640px block. */
.company { display: flex; flex-direction: row; align-items: center; justify-content: center; gap: 28px; }
.company-copy { flex: 0 1 auto; }
.company p { margin: 0; max-width: 62ch; color: var(--mist); }
.company .seclbl { margin-bottom: 8px; }

/* A cartoon on flat navy reads as clip art, so the figure gets a ground: a
   cocoa wash that fades out before the box edge, giving atmosphere rather than
   a visible container.

   Three transforms want the same element, so they are split across two: the
   wrapper carries the mirror that turns Meaty toward the copy plus the hover
   lift, and the image carries the idle float. An animation and a transition on
   one property would fight, and the animation would win. */
.company-mascot {
  flex: 0 0 auto;
  position: relative;
  width: 156px;
  transform: scaleX(-1);
  transition: transform 420ms cubic-bezier(0.22, 1, 0.36, 1);
}
.company-mascot::before {
  content: "";
  position: absolute;
  inset: -16%;
  border-radius: 50%;
  background: radial-gradient(circle,
    var(--orange-glow) 0%,
    var(--orange-glow-mid) 46%,
    var(--orange-glow-0) 72%);
  opacity: 0.85;
  transition: opacity 420ms ease;
}
.company-mascot img {
  position: relative;
  width: 100%;
  height: auto;
  display: block;
  animation: mascot-idle 5.5s ease-in-out infinite;
}
.company-mascot:hover { transform: scaleX(-1) scale(1.06); }
.company-mascot:hover::before { opacity: 1; }

/* a slow breath, not a bounce: 7px of travel over 5.5s reads as alive without
   pulling the eye off the paragraph beside it */
@keyframes mascot-idle {
  0%, 100% { transform: translateY(0) rotate(-1.1deg); }
  50%      { transform: translateY(-7px) rotate(1.1deg); }
}


/* ---------- full bio pages ---------- */

/* Bio page: portrait + identity as one composed card at every width.
   Desktop: sticky side column. Below 820px: a media-object card (photo
   beside identity), never a shrunken desktop stack. */
.bio-layout { display: grid; grid-template-columns: 340px 1fr; gap: 44px; align-items: start; }
.bio-side { position: sticky; top: 24px; display: grid; gap: 0 20px; }
.bio-photo { width: 100%; height: auto; display: block; border-radius: var(--r-lg); }
.bio-id { padding-top: 14px; }
.bio-links { display: flex; gap: 18px; align-items: center; margin: 12px 0 16px; flex-wrap: wrap; }
.bio-links .btn { padding: 11px 22px; font-size: var(--step--1); }
.bio-links .more { font-family: var(--font-mono); font-size: var(--step--2); letter-spacing: 0.1em; text-transform: uppercase; color: var(--orange-soft); }
.bio-prose { margin-inline: 0; max-width: 66ch; }
.bio-prose p { color: var(--mist); }

/* Credential matrix: the badge wall at the foot of a bio page, fed by
   _data/credentials.yml through _includes/credential-matrix.html. Replaces the
   single .badge-sm PECB hex that used to sit in the sidebar.

   Flex rather than grid, so each mark sizes to its own content. A fixed track
   would force a caption to wrap inside it, and these captions must stay on one
   line. No card ground and no border either: the marks float on the section,
   so the gap does the separating a box edge used to. */
.credgrid {
  display: flex;
  flex-wrap: wrap;
  /* Chris runs six marks and they have to hold one row. They measure 868px
     against the 1112px the container gives, so the column gap is what decides
     it: 38px leaves ~54px of slack, enough to survive the mono face falling
     back to a slightly wider system font. 44px left only 24px. */
  gap: 30px 38px;
  list-style: none;
  padding: 0;
  /* Space under the .seclbl heading. This collapses against that heading's own
     10px bottom margin rather than adding to it, so the number here is the
     whole gap, not part of it: 26px off the first row of marks. */
  margin: 26px 0 0;
}
.cred { display: grid; align-content: start; gap: 12px; }
/* One fixed mark box normalises logos sharing no aspect ratio: a square Navy
   seal, a 3.5:1 MITRE wordmark, a 2:1 longhorn and a taller-than-wide PECB hex
   all sit on the same optical baseline. Capping both axes and leaving width and
   height auto preserves each file's own ratio. */
/* The caption is usually wider than the mark, so the mark centers over it
   rather than hanging off its left edge. */
.cred-mark { display: flex; align-items: center; justify-content: center; min-height: 64px; }
/* max-width is a length, not a percentage: the flex item sizes to this image,
   so a percentage would be circular. In practice this caps exactly one mark,
   the MITRE wordmark, whose 3.5:1 ratio means it hits the width before it
   comes near the 64px height. So this number is the MITRE size: 152px is 80%
   of the 190px it originally ran at. */
.cred-mark img { display: block; max-height: 64px; max-width: 152px; width: auto; height: auto; }
.cred-type {
  font-family: var(--font-display);
  font-size: 1.75rem;
  font-weight: 800;
  line-height: 1;
  letter-spacing: -0.02em;
  color: #fff;
}
/* Captions never wrap on their own, which is why the mark sits in a flex item
   sizing to content rather than a fixed track. A caption needing two lines
   says so in the data and breaks where the author put the break: each
   .cred-line is its own nowrap row. */
.cred-note {
  display: grid;
  gap: 3px;
  font-family: var(--font-mono);
  font-size: var(--step--2);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--orange-soft);
}
.cred-line { white-space: nowrap; }

/* About founders: in-flow identity is a small-screen device; hidden on desktop
   where the name lives in the photo gradient */
.fid { display: none; }
.fid b { display: block; font-family: var(--font-display); font-size: 1.1rem; letter-spacing: -0.015em; color: #fff; }
.fid span { font-family: var(--font-mono); font-size: var(--step--2); letter-spacing: 0.1em; text-transform: uppercase; color: var(--orange-soft); }

/* ---------- responsive ---------- */

@media (max-width: 1023px) {
  .ribbon ul { grid-template-columns: repeat(2, 1fr); }
  .insights-row { grid-template-columns: 1fr; gap: 22px; }
  .media-row, .profile-header { grid-template-columns: 1fr; }
  .media-row--flip > .media-row-img { order: 0; }
}

/* The desktop nav needs ~874px to fit without overflowing. Switch to the
   hamburger at 900px so there is never a band where the nav is rendered but
   does not fit. The rest of the responsive rules stay at 820px (bio-layout,
   content grids, etc.) because they are independent of nav width. */
@media (max-width: 900px) {
  .nav-toggle { display: block; }

  /* --n-900 and a hard bottom edge, not the --n-800 this had: the panel drops
     onto the hero field, which is --n-800 itself, so the two grounds matched
     and the panel had no discernible edge. The hero text below the last item
     read as bleeding through an open menu. */
  .site-nav {
    position: absolute;
    top: 100%; left: 0; right: 0;
    background: var(--n-900);
    border-bottom: 1px solid var(--line-d-firm);
    display: none;
    padding-bottom: 12px;
    box-shadow: 0 12px 24px var(--shadow);
    /* Bottom two only: the panel hangs off the header, so its top edge is a
       join and not a corner. The same reasoning as the hero field below it,
       which this drops onto and whose top corners are square for the same
       reason. */
    border-radius: 0 0 var(--r-lg) var(--r-lg);
  }
  .site-nav.open { display: block; }
  .site-nav > ul { flex-direction: column; align-items: stretch; gap: 0; }
  .site-nav a { padding: 13px 24px; white-space: normal; }
  .nav-cta { margin: 8px 24px 0; text-align: center; }

  .sub-menu { position: static; display: none; background: var(--wash-d); border: 0; box-shadow: none; }
  .sub-menu .sub-menu { min-width: 0; }
  .sub-menu a { padding-left: 40px; }
  .has-sub:hover > .sub-menu { display: none; }
  .has-sub.open > .sub-menu { display: block; }
  .has-sub:focus-within > .sub-menu { display: none; }
  .has-sub.open:focus-within > .sub-menu { display: block; }
}

@media (max-width: 820px) {
  .bio-layout { grid-template-columns: 1fr; gap: 26px; }
  /* media object: photo beside identity, prose below */
  .bio-side { position: static; grid-template-columns: 220px 1fr; align-items: start; }
  .bio-photo { grid-row: 1 / 3; }
  .bio-id { padding-top: 0; }
  /* The credential band centers once the page stops being two columns: a
     left-ragged wall of two-per-row marks reads as a mistake at this width.
     The heading goes with it, or it hangs off the edge of a centered grid. */
  .credband .seclbl { text-align: center; }
  .credgrid { justify-content: center; }
  .cred-note { text-align: center; }
  .fname { padding: 32px 14px 12px; }
  .fname b { font-size: 1.05rem; }
  .statband { grid-template-columns: 1fr; }
  .statband .stat { border-right: 0; border-bottom: 1px solid var(--line-d-soft); padding: 20px 24px; }
  .statband .stat:last-child { border-bottom: 0; }

  .steps { grid-template-columns: 1fr; gap: 16px; }
  .offer { grid-template-columns: 1fr; gap: 10px; padding: 20px 0; }
  /* the spine can't stretch in a stacked grid, so lay it flat above the heading */
  .o-rule { width: 40px; height: 2px; min-height: 0; align-self: start; }
  .featured { padding: 20px 16px; }
  /* ATLAS rows fold: id + technique on one line, plain-English line beneath */
  .atlas-row { grid-template-columns: auto 1fr; gap: 5px 14px; }
  .atlas .t-what { grid-column: 1 / -1; }
  .atlas--plain .atlas-row { grid-template-columns: 1fr; }
  .featured + .featured { margin-top: 20px; }
  /* stacked, but still centred so the band reads the same at every width */
  .cta-band .container { flex-direction: column; gap: 14px; text-align: center; }
  .deliv { flex-direction: column; align-items: flex-start; }
  .section { padding: 48px 0; }
  .hero { padding: 48px 0 60px; }
  /* phones don't need the full desktop gap between header and headline */
  .hero-field .hero { padding-top: calc(var(--header-h) + 40px); }
  /* The two hero links sit inline on desktop. At phone width the ghost link
     wrapped under the solid button and hung off its left edge, which read as
     a mistake rather than a pair. Stack them to the full measure instead, so
     both span the same edges as the headline above them and each label sits
     centred in its own button. The headline and lead stay left-aligned. */
  .hero p:has(> .btn) { display: flex; flex-direction: column; align-items: stretch; gap: 12px; }
  .hero p:has(> .btn) .btn { width: 100%; }
  .page-hero { padding: calc(var(--header-h) + 40px) 0 52px; }
  .band .close { margin-left: 0; text-align: left; }
}


@media (max-width: 640px) {
  /* One service per line. This has to sit after the 1023px block above, not
     with the other 640px rules earlier in the file: both queries match on a
     phone and the two selectors are equally specific, so the later one wins. */
  .ribbon ul { grid-template-columns: 1fr; }

  /* founders become media-object cards: photo beside name/role, bio below */
  .founders { grid-template-columns: 1fr; gap: 34px; }
  .fcard { display: grid; grid-template-columns: 136px 1fr; gap: 4px 16px; align-items: center; }
  .fphoto { grid-row: 1; }
  .fphoto img { aspect-ratio: 1 / 1; height: auto; object-position: 50% 20%; }
  .fname { display: none; }
  .fid { display: block; grid-column: 2; }
  .fcard .fbio, .fcard .fmeta, .fcard .more { grid-column: 1 / -1; }
  .fcard .fbio { margin-top: 14px; }
  /* Meaty flanks the copy from 641px up, but 375px has no room for a figure
     and a 62ch paragraph side by side, so phones break the row into a stack:
     smaller Meaty, centred over the copy. Left-aligned he left ~210px of dead
     space beside him and read as a figure that had failed to lay out. The
     label and paragraph stay on the spine here: they line up with 23 and
     SDVOSB in the statband directly above, and centring them would break that. */
  .company { flex-direction: column; align-items: flex-start; gap: 18px; }
  .company-mascot { width: 118px; align-self: center; }
  /* A 132px chip leaves a title about 18 characters of line on a 375px screen,
     so the chip shrinks rather than the text column. */
  .pub { grid-template-columns: 84px minmax(0, 1fr); gap: 4px 16px; padding: 16px; }
  .pub-thumb { width: 84px; }
  .pub-thumb--mark { padding: 14px; }
}

@media (max-width: 460px) {
  .bio-side { grid-template-columns: 128px 1fr; }
  /* the bio card keeps its media-object shape, just tighter */
  .author-card { grid-template-columns: 76px 1fr; gap: 16px; }
  .prose .author-photo { width: 76px; height: 76px; }
  .bio-links .btn { padding: 9px 16px; }
  /* One mark per row on a phone. Two fit the width, but a two-up wall of
     mixed-height captions staggers badly, so it stacks instead. */
  .credgrid { flex-direction: column; align-items: center; gap: 24px; }
  .cred-mark { min-height: 52px; }
  .cred-mark img { max-height: 52px; max-width: 120px; }
  .cred-type { font-size: 1.45rem; }
}

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