/* Site base styles. Loaded globally via mu-plugin. Keep this lean. */

/* Self-hosted faces. Files live beside this stylesheet, so the paths are
   relative and survive a move between local, staging and production.
   Deliberately not loaded through Oxygen's font manager: Oxygen only emits
   a font's stylesheet when something in the builder references it, which
   silently drops the face when the family is set in CSS instead. Owning it
   here keeps the stack and the file in one place.
   No Google Fonts request is made — nothing leaves the visitor's browser. */
/* Latin subsets. The full Inter variable face is 344KB because it carries
   Greek, Cyrillic and Vietnamese; latin alone is 47KB. latin-ext is declared
   separately with its own unicode-range, so it is only fetched if a character
   actually needs it — in practice this site downloads 47KB + 30KB.
   JetBrains Mono is variable here, so one file covers 400 and 500. */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url('fonts/Inter-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url('fonts/Inter-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
    U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 100 800;
  font-display: swap;
  src: url('fonts/JetBrainsMono-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}

:root {
  /* Colour
     Green/black palette for the cyber-security positioning.
     Neutrals carry a faint green bias so surfaces sit in the same
     family as the accent rather than reading as blue-grey against it.
     --c-ok is deliberately equal to --c-accent: on a security site
     "safe" and "the brand" are the same claim. Critical and warning
     carry all the real signal. */
  --c-bg: #050807;
  --c-surface: #0C120E;
  --c-raised: #141C17;
  --c-border: #1E2A22;
  --c-border-2: #2E4034;
  --c-text: #F4F8F5;
  --c-muted: #9DB0A4;
  --c-faint: #7A8C80;
  --c-accent: #2EE86B;
  --c-accent-2: #6BFFA0;
  --c-accent-ink: #032011;
  --c-critical: #F4646E;
  --c-warning: #F0B23F;
  --c-ok: #2EE86B;
  --c-focus: #7DFFB8;

  /* Type — fluid, so no per-breakpoint overrides to maintain */
  --font-sans: Inter, system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-mono: "JetBrains Mono", ui-monospace, Consolas, monospace;
  --t-sm: clamp(0.937rem, 0.90rem + 0.16vw, 1rem);
  --t-base: clamp(1rem, 0.96rem + 0.22vw, 1.125rem);
  --t-lg: clamp(1.2rem, 1.13rem + 0.35vw, 1.406rem);
  --t-xl: clamp(1.44rem, 1.33rem + 0.55vw, 1.758rem);
  --t-2xl: clamp(1.728rem, 1.56rem + 0.84vw, 2.197rem);
  --t-3xl: clamp(2.074rem, 1.83rem + 1.22vw, 2.747rem);
  --t-4xl: clamp(2.488rem, 2.13rem + 1.79vw, 3.433rem);

  /* Space — 4px base. Use only these. */
  --s-3xs: 0.25rem;
  --s-2xs: 0.5rem;
  --s-xs: 0.75rem;
  --s-sm: 1rem;
  --s-md: 1.5rem;
  --s-lg: 2rem;
  --s-xl: 3rem;
  --s-2xl: 4rem;
  /* Section padding, applied top and bottom, so the gap between two
     sections is twice this. The old clamp(3.5rem, 2.5rem + 5vw, 7.5rem)
     resolved to 104px at 1280px wide — 208px of empty ground between
     every section, and a 6000px homepage carrying 700 words. Air is what
     makes a page feel considered, but past a point it just makes the
     reader scroll through nothing. This lands at 56px on desktop and
     40px on a phone. */
  --section-y: clamp(2.5rem, 1.25rem + 2.5vw, 4.5rem);
  --gutter: clamp(1rem, 0.5rem + 2.5vw, 2.5rem);

  /* Layout */
  --w-max: 1200px;
  --w-narrow: 720px;

  /* Radius */
  --r-sm: 4px;
  --r-md: 8px;
  --r-lg: 14px;
  --r-full: 999px;

  /* Motion */
  --ease: cubic-bezier(0.16, 1, 0.3, 1);
  --dur: 180ms;
}

/* NOTE ON THE `html` / `body` PREFIXES BELOW.
   Oxygen emits global-settings.css AFTER this file, containing:
       body { background-color: var(--bde-background-color); color: ... }
       h1,h2,h3,h4,h5,h6 { color: var(--bde-headings-color) }
       h1 { font-size: var(--bde-h1-font-size) }   <- 16px * 1.25^5
   Those are bare element selectors, so at equal specificity Oxygen wins on
   source order and the site renders on its light default with a fixed
   modular type scale. Enqueue priority does not help — Oxygen registers
   later regardless.
   Oxygen's base-size x ratio model cannot express a fluid clamp(), so the
   scale below cannot be mirrored into its Globals panel. One extra element
   in the selector (0,0,2 vs 0,0,1) settles it without !important.
   Per-element builder styling uses generated .oxy-* classes and is more
   specific than this, so the builder still wins where it should. */

html body {
  background: var(--c-bg);
  color: var(--c-text);
  font: var(--t-base)/1.65 var(--font-sans);
  -webkit-font-smoothing: antialiased;
}

body h1, body h2, body h3, body h4 {
  color: var(--c-text);
  line-height: 1.15;
  letter-spacing: -0.02em;
  font-weight: 600;
  text-wrap: balance;
}

body h1 { font-size: var(--t-4xl); }
body h2 { font-size: var(--t-3xl); }
body h3 { font-size: var(--t-xl); line-height: 1.35; }
body h4 { font-size: var(--t-lg); line-height: 1.35; }

p, li { color: var(--c-muted); text-wrap: pretty; }

body a {
  color: var(--c-accent);
  text-underline-offset: 0.2em;
  transition: color var(--dur) var(--ease);
}
body a:hover { color: var(--c-accent-2); }

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

/* Keyboard focus. Non-negotiable on a site selling security. */
:focus-visible {
  outline: 2px solid var(--c-focus);
  outline-offset: 2px;
  border-radius: var(--r-sm);
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

/* Utilities — build these as classes in Oxygen, apply, never restyle inline. */

.section { padding: var(--section-y) var(--gutter); }

/* Every section was an identically-padded slab floating in the same
   black, so a page read as a stack of unrelated panels rather than one
   document. A hairline between consecutive sections gives the page a
   spine — the same trick a printed page uses — and costs one border.
   Only between siblings, so a page never opens or closes on a stray rule. */
.section + .section { border-top: 1px solid var(--c-border); }

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

/* Narrow sections used to centre their own 720px column, which put them
   240px inside the left edge that the hero and the card rows sit on. Two
   competing left margins down one page reads as an accident rather than
   a decision. The container stays the full 1200 and the measure is
   applied to its children instead, so every section starts on the same
   line and the page gets a spine to hang off. */
.container--narrow { max-width: var(--w-max); }
.container--narrow > * { max-width: var(--w-narrow); }

.lead { font-size: var(--t-lg); color: var(--c-muted); max-width: 60ch; }

/* Headings keep the browser's default margin — 0.83em, which is 36px on
   an h2 at this size — and Oxygen wraps each one in its own rich-text
   div. That div is a flex item, so the margin cannot collapse out of it
   and instead pads the box from the inside, adding roughly 72px per
   section head that the container's `gap` does not control and cannot
   see. The gap owns the rhythm in these stacks, so the block's outermost
   margins are redundant; inner ones are left alone, which keeps multi-
   paragraph blocks reading correctly.

   .post-body and .section--doc are excluded: both manage their own
   vertical rhythm deliberately and are handled further down. */
.sec-head .oxy-rich-text > :first-child,
[class*="stack-"] > .oxy-rich-text > :first-child { margin-top: 0; }

.sec-head .oxy-rich-text > :last-child,
[class*="stack-"] > .oxy-rich-text > :last-child { margin-bottom: 0; }

/* Section marker.
   Was mono, uppercase, letter-spaced and in the accent green — which is
   the single most recognisable piece of template furniture going, and it
   appeared above every section on every page. Same text, same job, but
   set in the body face at reading case so it reads as a quiet label
   rather than a neon tag. The accent is worth more when it is spent on
   fewer things. */
.eyebrow {
  font-family: var(--font-sans);
  font-size: var(--t-sm);
  text-transform: none;
  letter-spacing: 0;
  color: var(--c-faint);
}

/* Card.
   A row of three rounded, bordered, lifting-on-hover boxes is the other
   thing every generated site does. These carry the same content as
   columns in a ruled table instead: one hairline above each, no fill, no
   radius, no lift. The rule does the separating that the border used to,
   using less ink, and the columns read as part of the page rather than
   as objects floating on it. */
.card {
  background: none;
  border: 0;
  border-top: 1px solid var(--c-border-2);
  border-radius: 0;
  padding: var(--s-md) 0 0;
  transition: border-color var(--dur) var(--ease);
}
.card:hover { border-top-color: var(--c-faint); }

/* The little accent dash sat on top of each card to give it a lid. The
   hairline does that now, so it would be saying the same thing twice. */
.card .rule-accent { display: none; }

/* Auto-fitting grid — no breakpoint column counts to maintain.
   Wider gutters now the cards have no borders: without them the columns
   need the white space to stay legible as separate things. */
.grid-auto {
  display: grid;
  gap: var(--s-xl);
  grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr));
}

.btn {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2xs);
  padding: var(--s-xs) var(--s-md);
  border: 1px solid transparent;
  /* Not --r-md. A softly rounded filled pill beside a ghost pill is the
     stock hero button pair; squaring them off costs nothing and stops
     the pattern being the first thing you recognise. */
  border-radius: 3px;
  font-size: var(--t-sm);
  font-weight: 500;
  text-decoration: none;
  cursor: pointer;
  transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.btn--primary { background: var(--c-accent); border-color: var(--c-accent); color: var(--c-accent-ink); }
.btn--primary:hover { background: var(--c-accent-2); border-color: var(--c-accent-2); color: var(--c-accent-ink); }
.btn--secondary { color: var(--c-text); border-color: var(--c-border-2); }
.btn--secondary:hover { background: var(--c-raised); border-color: var(--c-faint); }

/* Severity badge — case studies, audit findings */
.badge {
  display: inline-block;
  font-family: var(--font-mono);
  font-size: var(--t-sm);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  padding: var(--s-3xs) var(--s-2xs);
  border: 1px solid currentColor;
  border-radius: var(--r-sm);
}
.badge--critical { color: var(--c-critical); }
.badge--warning { color: var(--c-warning); }
.badge--ok { color: var(--c-ok); }

/* Vertical stacks. Gap-based, so no collapsing or doubling margins. */
.stack-sm { display: flex; flex-direction: column; gap: var(--s-2xs); }
.stack-md { display: flex; flex-direction: column; gap: var(--s-md); }
.stack-lg { display: flex; flex-direction: column; gap: var(--s-lg); }
.stack-xl { display: flex; flex-direction: column; gap: var(--s-xl); }

/* Section header — eyebrow + heading + lead, capped so it stays readable. */
.sec-head { display: flex; flex-direction: column; gap: var(--s-xs); max-width: 62ch; }

/* ---------------------------------------------------------------
   Scan panel. A rendered assessment readout — the one component
   that has to look like real output rather than marketing.
   --------------------------------------------------------------- */
.panel {
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: var(--r-lg);
  overflow: hidden;
}
.panel__bar {
  display: flex; align-items: center; gap: var(--s-2xs);
  padding: var(--s-xs) var(--s-sm);
  border-bottom: 1px solid var(--c-border);
  background: var(--c-bg);
  font-family: var(--font-mono); font-size: var(--t-sm); color: var(--c-faint);
}
.panel__prompt { color: var(--c-accent); }
.panel__url { color: var(--c-muted); }
.panel__live { margin-left: auto; display: inline-flex; align-items: center; gap: var(--s-3xs); color: var(--c-accent); }
.panel__pulse {
  width: 6px; height: 6px; border-radius: var(--r-full);
  background: var(--c-accent);
  animation: panel-pulse 2.4s var(--ease) infinite;
}
@keyframes panel-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.2; } }

/* Severity tally — counts by severity, four across. */
.tally { display: grid; grid-template-columns: repeat(4, 1fr); }
.tally__cell {
  padding: var(--s-md) var(--s-xs);
  display: flex; flex-direction: column; align-items: center; gap: var(--s-3xs);
  border-right: 1px solid var(--c-border);
}
.tally__cell:last-child { border-right: 0; }
.tally__n {
  font-family: var(--font-mono); font-size: var(--t-2xl); line-height: 1;
  font-variant-numeric: tabular-nums; font-weight: 500;
}
.tally__n--critical { color: var(--c-critical); }
.tally__n--high { color: var(--c-warning); }
.tally__n--medium { color: var(--c-muted); }
.tally__n--fixed { color: var(--c-accent); }
.tally__l {
  font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--c-faint);
}

/* Findings list — one row per finding, severity badge right-aligned. */
.findings { border-top: 1px solid var(--c-border); }
.finding {
  display: flex; align-items: center; gap: var(--s-sm);
  padding: var(--s-xs) var(--s-sm);
  border-bottom: 1px solid var(--c-border);
}
.finding:last-child { border-bottom: 0; }
.finding__text { color: var(--c-muted); font-family: var(--font-mono); font-size: 0.82rem; }
.finding__badge { margin-left: auto; flex-shrink: 0; }

/* ---------------------------------------------------------------
   Metric pair — a number and its label. Used in case studies.
   --------------------------------------------------------------- */
.stat { display: flex; flex-direction: column; gap: 2px; }
.stat__n {
  font-family: var(--font-mono); font-size: var(--t-xl); color: var(--c-accent);
  font-variant-numeric: tabular-nums; line-height: 1;
}
.stat__l {
  font-family: var(--font-mono); font-size: 0.7rem; text-transform: uppercase;
  letter-spacing: 0.06em; color: var(--c-faint);
}
/* Row of metrics pinned to the bottom of a card, above a hairline. */
.stat-row {
  display: flex; gap: var(--s-lg); flex-wrap: wrap;
  padding-top: var(--s-sm); border-top: 1px solid var(--c-border); margin-top: auto;
}

/* ---------------------------------------------------------------
   Numbered step. Only use where the order actually carries meaning.
   --------------------------------------------------------------- */
.step {
  display: flex; flex-direction: column; gap: var(--s-2xs);
  padding-top: var(--s-sm); border-top: 1px solid var(--c-border);
}
.step__n {
  font-family: var(--font-mono); font-size: var(--t-sm);
  color: var(--c-accent); font-variant-numeric: tabular-nums;
}

/* Short accent rule above a card heading. */
.rule-accent {
  width: 28px; height: 2px; background: var(--c-accent);
  border-radius: var(--r-full);
}

/* ---------------------------------------------------------------
   Site header. Layout normally belongs to Oxygen, but a sticky
   flex bar is fiddly to maintain through a builder UI and it is
   identical on every page, so it lives here.
   --------------------------------------------------------------- */
.nav {
  position: sticky; top: 0; z-index: 20;
  background: color-mix(in srgb, var(--c-bg) 88%, transparent);
  backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--c-border);
}
.nav__inner {
  max-width: var(--w-max); margin-inline: auto;
  padding: var(--s-sm) var(--gutter);
  display: flex; align-items: center; gap: var(--s-md);
}
/* Wordmark. Set as one word — the internal capitals do the word
   separation, so no uppercase transform: JAMESKDESIGNS would be an
   unreadable run of thirteen caps. Tracking eased to suit mixed case. */
.nav__mark {
  font-family: var(--font-mono); font-size: var(--t-base);
  letter-spacing: 0.01em;
  color: var(--c-text); text-decoration: none; font-weight: 500;
  display: inline-flex; align-items: center; gap: var(--s-2xs);
  white-space: nowrap;
}
.nav__mark::before {
  content: ""; width: 7px; height: 7px;
  border-radius: var(--r-full); background: var(--c-accent);
}
.nav__links { margin-left: auto; display: flex; gap: var(--s-md); align-items: center; }
.nav__links a {
  font-family: var(--font-mono); font-size: var(--t-sm);
  color: var(--c-muted); text-decoration: none;
}
.nav__links a:hover { color: var(--c-text); }
.nav__links a.btn { font-family: var(--font-sans); color: var(--c-accent-ink); }
.nav__links a.btn:hover { color: var(--c-accent-ink); }

/* Hamburger toggle. Hidden on desktop; the links stand on their own there. */
.nav__toggle {
  display: none;
  margin-left: auto;
  background: transparent;
  border: 1px solid var(--c-border-2);
  border-radius: var(--r-md);
  padding: var(--s-2xs);
  cursor: pointer;
  color: var(--c-text);
  line-height: 0;
  transition: border-color var(--dur) var(--ease), background var(--dur) var(--ease);
}
.nav__toggle:hover { background: var(--c-raised); border-color: var(--c-faint); }

/* Three bars drawn from one element: the middle is the box, the outer two
   are pseudo-elements. Collapses to an X when open. */
.nav__bars,
.nav__bars::before,
.nav__bars::after {
  display: block;
  width: 20px; height: 2px;
  background: currentColor;
  border-radius: var(--r-full);
  transition: transform var(--dur) var(--ease), opacity var(--dur) var(--ease);
}
.nav__bars { position: relative; }
.nav__bars::before,
.nav__bars::after { content: ""; position: absolute; left: 0; }
.nav__bars::before { top: -6px; }
.nav__bars::after  { top: 6px; }

.nav.is-open .nav__bars { background: transparent; }
.nav.is-open .nav__bars::before { transform: translateY(6px) rotate(45deg); }
.nav.is-open .nav__bars::after  { transform: translateY(-6px) rotate(-45deg); }

@media (max-width: 760px) {
  .nav__toggle { display: block; }

  /* The link row becomes a panel dropping out of the bar. */
  .nav__links {
    position: absolute; top: 100%; left: 0; right: 0;
    margin-left: 0;
    flex-direction: column; align-items: stretch;
    gap: var(--s-xs);
    padding: var(--s-sm) var(--gutter) var(--s-md);
    background: var(--c-surface);
    border-bottom: 1px solid var(--c-border);
    display: none;
  }
  .nav.is-open .nav__links { display: flex; }

  .nav__links a { font-size: var(--t-base); padding: var(--s-2xs) 0; }
  .nav__links a.btn { justify-content: center; margin-top: var(--s-2xs); }
}

/* ---------------------------------------------------------------
   Hero. Asymmetric: argument on the left, evidence on the right.
   --------------------------------------------------------------- */
.hero { padding: var(--section-y) var(--gutter) var(--s-2xl); }
.hero__grid {
  max-width: var(--w-max); margin-inline: auto;
  display: grid; gap: var(--s-xl);
  grid-template-columns: 1fr;
  align-items: center;
}
@media (min-width: 900px) {
  .hero__grid { grid-template-columns: 1.02fr 0.98fr; gap: var(--s-2xl); }
}
.hero__copy { display: flex; flex-direction: column; gap: var(--s-md); align-items: flex-start; }
.hero__copy h1 { max-width: 16ch; }
/* The last clause of the headline used to be set in the accent green.
   Colouring one phrase of a hero headline is the most-copied move on the
   web, and here it also split the sentence in half at exactly the point
   where it should land as one thought. It reads as a single statement
   now; the mark beside it carries the green. */
.hero h1 em { font-style: normal; color: inherit; }
.hero__actions { display: flex; flex-wrap: wrap; gap: var(--s-xs); }
/* Body face, not mono. A monospaced location line under a hero button
   pair is the same borrowed-terminal costume as the service lists were;
   with those changed this was the last of it above the fold. */
.hero__meta {
  font-family: var(--font-sans); font-size: var(--t-sm);
  color: var(--c-faint); display: flex; flex-wrap: wrap; gap: var(--s-xs) var(--s-md);
}

/* ---------------------------------------------------------------
   Case studies — two up, metrics pinned to the bottom edge.
   --------------------------------------------------------------- */
.work { display: grid; gap: var(--s-md); grid-template-columns: 1fr; }
@media (min-width: 860px) { .work { grid-template-columns: 1fr 1fr; } }
.case { display: flex; flex-direction: column; gap: var(--s-sm); padding: var(--s-lg); }
.case__top { display: flex; align-items: baseline; gap: var(--s-sm); flex-wrap: wrap; }
.case__tag {
  font-family: var(--font-mono); font-size: var(--t-sm);
  color: var(--c-faint); margin-left: auto;
}

/* Process row. Auto-fitting, so the step count can change without edits. */
.steps {
  display: grid; gap: var(--s-md);
  grid-template-columns: repeat(auto-fit, minmax(min(220px, 100%), 1fr));
}

/* Closing call to action. */
.cta {
  max-width: var(--w-max); margin-inline: auto;
  background: var(--c-surface); border: 1px solid var(--c-border);
  border-radius: var(--r-lg); padding: var(--s-xl) var(--s-lg);
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-md);
}
.cta__copy { display: flex; flex-direction: column; gap: var(--s-2xs); flex: 1 1 320px; }

/* Site footer. Same reasoning as .nav — identical on every page. */
.foot { border-top: 1px solid var(--c-border); padding: var(--s-lg) var(--gutter); }
.foot__inner {
  max-width: var(--w-max); margin-inline: auto;
  display: flex; flex-wrap: wrap; gap: var(--s-sm) var(--s-md); align-items: center;
  font-family: var(--font-mono); font-size: var(--t-sm); color: var(--c-faint);
}
.foot__inner a { color: var(--c-faint); text-decoration: none; }
.foot__inner a:hover { color: var(--c-accent); }
.foot__right { margin-left: auto; display: flex; gap: var(--s-md); flex-wrap: wrap; }

/* Mono spec list with arrow markers — service inclusions, scope items. */
/* The class sits on the Oxygen RichText element and the <ul> lives inside its
   content, so this targets the descendant list rather than the element itself.
   That keeps real <ul>/<li> semantics — a screen reader still announces "list,
   4 items" — while the items stay editable in the builder's WYSIWYG. */
.spec-list ul {
  list-style: none; padding: 0; margin: 0;
  display: flex; flex-direction: column; gap: var(--s-3xs);
}
/* These are plain service lists, not terminal output. Setting them in
   mono with an arrow marker borrowed the look of a readout for content
   that has not earned it — and mono everywhere is what makes a site read
   as costume. The .panel below is real rendered output and keeps its
   mono; this does not. */
.spec-list li {
  font-family: var(--font-sans); font-size: var(--t-sm); color: var(--c-muted);
  padding-left: var(--s-sm); position: relative;
}
.spec-list li::before {
  content: ""; position: absolute; left: 0; top: 0.72em;
  width: 7px; height: 1px; background: var(--c-border-2);
}

/* Process step titles are h3 for correct document outline (the section
   heading is h2, so h4 would skip a level). Sized down to h4 scale here
   rather than choosing the tag for its default size. */
.step h3 { font-size: var(--t-lg); line-height: 1.35; }

/* Row of severity chips — used on the audits section to show what a
   report's findings look like without inventing counts. */
.chip-row { display: flex; flex-wrap: wrap; gap: var(--s-2xs); align-items: center; }

/* Medium sits below High in severity, so it reads in muted rather than
   inheriting the brighter body text colour. */
.badge--medium { color: var(--c-muted); }

/* Unfilled placeholder. Deliberately loud — content that reaches production
   with one of these still in it is worse than a gap, so it should be
   impossible to scroll past. Delete the class once the real copy is in. */
.placeholder {
  border: 1px dashed var(--c-warning);
  border-radius: var(--r-md);
  padding: var(--s-sm) var(--s-md);
  background: color-mix(in srgb, var(--c-warning) 8%, transparent);
}
.placeholder::before {
  content: "NEEDS YOUR INPUT";
  display: block;
  font-family: var(--font-mono);
  font-size: 0.68rem;
  letter-spacing: 0.1em;
  color: var(--c-warning);
  margin-bottom: var(--s-2xs);
}
.placeholder p, .placeholder li { color: var(--c-warning); }

/* ---------------------------------------------------------------
   Portfolio showcase — screenshot above, description below.
   The frame holds a fixed aspect ratio so the grid stays even
   whether or not a screenshot has been added yet.
   --------------------------------------------------------------- */
.shot {
  aspect-ratio: 16 / 10;
  border-radius: var(--r-md);
  overflow: hidden;
  background: var(--c-raised);
  border: 1px solid var(--c-border);
  display: grid;
  place-items: center;
  margin-bottom: var(--s-sm);
}
.shot img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: top center; /* websites read from the top */
  display: block;
}

/* Card wrapping a screenshot: tighter padding, image sits flush-ish */
.shot-card { padding: var(--s-sm); }
.shot-card h3 { margin-bottom: var(--s-3xs); }
.shot-card p { font-size: var(--t-sm); }

/* Small meta line under a project title */
.shot-meta {
  font-family: var(--font-mono);
  font-size: 0.78rem;
  color: var(--c-faint);
  letter-spacing: 0.04em;
}


/* ---------------------------------------------------------------
   FAQ accordion, built from Oxygen elements so the copy stays
   editable in the builder.

   Progressive enhancement: without JavaScript every answer is
   visible, which is both a usable fallback and better for search.
   nav.js adds [data-faq-ready] and the ARIA wiring, and only then
   do the answers collapse.
   --------------------------------------------------------------- */
.faq { display: flex; flex-direction: column; gap: var(--s-2xs); }

.faq-item {
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: var(--r-md);
  transition: border-color var(--dur) var(--ease);
}
.faq-item.is-open { border-color: var(--c-border-2); }

.faq-q {
  padding: var(--s-sm) var(--s-md);
  display: flex;
  align-items: center;
  gap: var(--s-sm);
  border-radius: var(--r-md);
}
.faq-q h3 {
  font-size: var(--t-base);
  font-weight: 600;
  line-height: 1.35;
  margin: 0;
}

/* Only behave like a control once JS has taken over. */
.faq[data-faq-ready] .faq-q { cursor: pointer; transition: background var(--dur) var(--ease); }
.faq[data-faq-ready] .faq-q:hover { background: var(--c-raised); }

/* Chevron from borders — no icon font, no extra request. */
.faq[data-faq-ready] .faq-q::after {
  content: "";
  margin-left: auto;
  flex-shrink: 0;
  width: 8px;
  height: 8px;
  border-right: 2px solid var(--c-accent);
  border-bottom: 2px solid var(--c-accent);
  transform: rotate(45deg) translate(-2px, -2px);
  transition: transform var(--dur) var(--ease);
}
.faq[data-faq-ready] .faq-item.is-open .faq-q::after { transform: rotate(-135deg); }

.faq-a { padding: 0 var(--s-md) var(--s-md); }
.faq-a p { font-size: var(--t-sm); margin: 0; }

/* Collapsed only when the script is running. */
.faq[data-faq-ready] .faq-item:not(.is-open) .faq-a { display: none; }

/* ===============================================================
   MOBILE REFINEMENTS

   The layout is already mobile-first — every auto-fitting grid uses
   minmax(min(Npx, 100%), 1fr), and that min() guard is what stops a
   track from forcing horizontal scroll on a narrow screen. Desktop
   columns are added at 860/900px rather than taken away.

   What follows fixes the three things that analysis showed the
   mobile-first defaults do not cover.
   =============================================================== */

/* 1. Severity tally.
      repeat(4, 1fr) leaves roughly 85px per cell at 375px, and
      "Resolved" at 0.68rem with 0.08em tracking needs about 78px.
      Two rows of two is legible instead of four cramped columns. */
@media (max-width: 520px) {
  .tally { grid-template-columns: repeat(2, 1fr); }
  .tally__cell:nth-child(2) { border-right: 0; }
  .tally__cell:nth-child(-n+2) { border-bottom: 1px solid var(--c-border); }
}

/* 2. Tap targets.
      .btn computes to roughly 39px tall, under the 44px that WCAG
      2.5.5 and both mobile platform guidelines ask for. Only raised
      on touch-ish widths so desktop keeps its tighter proportions. */
@media (max-width: 760px) {
  .btn {
    min-height: 44px;
    padding-inline: var(--s-md);
  }

  /* Nav panel links: give the whole row a comfortable target rather
     than only the text. */
  .nav__links a:not(.btn) {
    min-height: 44px;
    display: flex;
    align-items: center;
  }

  /* Metric rows: 32px between three stats is wasteful on a phone and
     pushes the third onto its own line for no reason. */
  .stat-row { gap: var(--s-md); }

  /* Case study tag sits under the title rather than fighting it for
     space on a narrow card. */
  .case__top { gap: var(--s-3xs); }
  .case__tag { margin-left: 0; }
}

/* 3. Very narrow phones (320–360px).
      Cards keep their generous desktop padding otherwise, which eats
      a meaningful share of the available width. */
@media (max-width: 400px) {
  .card { padding: var(--s-sm); }
  .cta { padding: var(--s-lg) var(--s-md); }
  .shot-card { padding: var(--s-2xs); }
}

/* ---------------------------------------------------------------
   "How it works" — three numbered steps, centred.

   Sits on --c-surface rather than the page ground so it reads as a
   distinct band, which is what gives the pattern its rhythm on a long
   page. The numerals are drawn in CSS: no icon font, no SVG requests,
   and they scale with the type rather than pixelating.
   --------------------------------------------------------------- */
.howto { background: var(--c-surface); border-block: 1px solid var(--c-border); }

.howto__head { text-align: center; max-width: 56ch; margin-inline: auto; }
.howto__head .eyebrow { display: block; }

.howto__grid {
  display: grid;
  gap: var(--s-xl);
  grid-template-columns: repeat(auto-fit, minmax(min(260px, 100%), 1fr));
}

.howto__step { text-align: center; display: flex; flex-direction: column; align-items: center; gap: var(--s-2xs); }

.howto__num {
  width: 84px;
  height: 84px;
  border-radius: var(--r-full);
  display: grid;
  place-items: center;
  background: var(--c-raised);
  border: 2px solid var(--c-accent);
  color: var(--c-accent);
  font-family: var(--font-mono);
  font-size: var(--t-2xl);
  font-weight: 500;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  margin-bottom: var(--s-2xs);
}

.howto__step h3 { font-size: var(--t-lg); }
.howto__step p { font-size: var(--t-sm); max-width: 34ch; }

@media (max-width: 760px) {
  .howto__grid { gap: var(--s-lg); }
  .howto__num { width: 68px; height: 68px; font-size: var(--t-xl); }
}

/* ---------------------------------------------------------------
   404 brand mark.
   The same JK mark used as the favicon, shown large. Rounded to match
   the icon's own corner radius so it reads as one identity across the
   tab strip and the page.
   --------------------------------------------------------------- */
.error-mark {
  width: 88px;
  height: 88px;
  border-radius: 18px;
  display: block;
  margin-bottom: var(--s-sm);
}

@media (max-width: 520px) {
  .error-mark { width: 68px; height: 68px; border-radius: 14px; }
}

/* ===============================================================
   FORM BUILDER

   Oxygen ships forms styled for a light theme — white inputs, blue
   submit — which is why the contact form arrived looking borrowed.
   These rules bring it into the design system.

   Selectors are doubled (.breakdance-form .breakdance-form-field__input
   rather than the single class) for the same reason the body and heading
   rules carry an html/body prefix: Oxygen emits global-settings.css after
   this file, and at equal specificity its defaults win on source order.
   =============================================================== */

.breakdance-form { display: flex; flex-direction: column; gap: var(--s-md); }

.breakdance-form .breakdance-form-field { display: flex; flex-direction: column; gap: var(--s-2xs); }

.breakdance-form .breakdance-form-field__label {
  font-family: var(--font-sans);
  font-size: var(--t-sm);
  font-weight: 500;
  color: var(--c-text);
  line-height: 1.4;
}

/* Required marker: warning amber rather than critical red. Nothing has
   gone wrong yet — it is guidance, not an error. */
.breakdance-form .breakdance-form-field__required { color: var(--c-warning); margin-left: 2px; }

.breakdance-form .breakdance-form-field__input,
.breakdance-form textarea.breakdance-form-field__input,
.breakdance-form select.breakdance-form-field__input {
  width: 100%;
  background: var(--c-bg);
  color: var(--c-text);
  border: 1px solid var(--c-border-2);
  border-radius: var(--r-md);
  padding: var(--s-xs) var(--s-sm);
  font-family: var(--font-sans);
  font-size: var(--t-base);
  line-height: 1.5;
  min-height: 44px;                /* same tap target as .btn */
  transition: border-color var(--dur) var(--ease), background var(--dur) var(--ease);
}

.breakdance-form textarea.breakdance-form-field__input {
  min-height: 160px;
  resize: vertical;
}

.breakdance-form .breakdance-form-field__input::placeholder { color: var(--c-faint); }

.breakdance-form .breakdance-form-field__input:hover { border-color: var(--c-faint); }

.breakdance-form .breakdance-form-field__input:focus,
.breakdance-form .breakdance-form-field__input:focus-visible {
  outline: 2px solid var(--c-focus);
  outline-offset: 1px;
  border-color: var(--c-accent);
  background: var(--c-surface);
}

/* Kill the yellow autofill wash Chrome paints over dark inputs. */
.breakdance-form .breakdance-form-field__input:-webkit-autofill,
.breakdance-form .breakdance-form-field__input:-webkit-autofill:focus {
  -webkit-text-fill-color: var(--c-text);
  -webkit-box-shadow: 0 0 0 1000px var(--c-surface) inset;
  caret-color: var(--c-text);
}

/* Submit: identical to .btn--primary so there is one button style on the
   site rather than two that nearly match. */
.breakdance-form .breakdance-form-button__submit,
.breakdance-form .button-atom--primary {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-2xs);
  min-height: 44px;
  padding: var(--s-xs) var(--s-lg);
  background: var(--c-accent);
  border: 1px solid var(--c-accent);
  border-radius: var(--r-md);
  color: var(--c-accent-ink);
  font-family: var(--font-sans);
  font-size: var(--t-sm);
  font-weight: 500;
  cursor: pointer;
  transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease);
}

.breakdance-form .breakdance-form-button__submit:hover,
.breakdance-form .button-atom--primary:hover {
  background: var(--c-accent-2);
  border-color: var(--c-accent-2);
  color: var(--c-accent-ink);
}

.breakdance-form .breakdance-form-footer { margin-top: var(--s-2xs); }

/* Success and error messages */
.breakdance-form .breakdance-form__message,
.breakdance-form .breakdance-form-message {
  border-radius: var(--r-md);
  padding: var(--s-xs) var(--s-sm);
  font-size: var(--t-sm);
  border: 1px solid currentColor;
}
.breakdance-form .breakdance-form__message--success { color: var(--c-accent); }
.breakdance-form .breakdance-form__message--error { color: var(--c-critical); }

/* ---------------------------------------------------------------
   Contact layout — details beside the form on desktop, stacked on
   mobile. The form is the primary action so it takes the wider
   column; the details sit beside it rather than above, which keeps
   the form above the fold on a laptop.
   --------------------------------------------------------------- */
.contact-grid {
  display: grid;
  gap: var(--s-xl);
  grid-template-columns: 1fr;
  align-items: start;
}

@media (min-width: 900px) {
  .contact-grid { grid-template-columns: 0.85fr 1.15fr; gap: var(--s-2xl); }
}

/* Details panel */
.contact-details { display: flex; flex-direction: column; gap: var(--s-md); }

.contact-detail { display: flex; flex-direction: column; gap: var(--s-3xs); }
.contact-detail a { font-size: var(--t-lg); font-weight: 500; word-break: break-word; }
.contact-detail p { margin: 0; font-size: var(--t-sm); }

/* The form sits in a card so it reads as the thing to fill in */
.form-card { padding: var(--s-lg); }
@media (max-width: 520px) { .form-card { padding: var(--s-md); } }

/* ===============================================================
   OXYGEN BRAND VARIABLES

   Oxygen colours its own components — form buttons, links, focus
   rings — from --bde-brand-primary-color, which defaults to
   --blue-500 (#3b82f6). That is where the blue submit button came
   from, and it would come back on any other Oxygen component too.

   Repointing the variable fixes all of them at once, which is
   better than overriding each component as it appears.

   Selector is html:root (0,1,1) rather than :root (0,1,0):
   global-settings.css is emitted after this file, so a plain :root
   here would lose to Oxygen's on source order.
   =============================================================== */
html:root {
  --bde-brand-primary-color: var(--c-accent);
  --bde-brand-primary-color-hover: var(--c-accent-2);

  /* Dark ink on the green, matching .btn--primary */
  --bde-button-primary-text-color: var(--c-accent-ink);
  --bde-button-primary-text-color-hover: var(--c-accent-ink);

  /* Inputs, so Oxygen's own form styling agrees with ours */
  --bde-form-input-background-color: var(--c-bg);
  --bde-form-input-focused-background-color: var(--c-surface);
  --bde-form-input-border-color: var(--c-border-2);
  --bde-form-input-focused-border-color: var(--c-accent);
  --bde-form-input-placeholder-color: var(--c-faint);
  --bde-form-text-color: var(--c-text);
  --bde-form-label-color: var(--c-text);
  --bde-form-input-border-radius: var(--r-md);
}

/* ===============================================================
   BLOG

   Long-form reading has different needs to a marketing page: a
   narrower measure, more space between blocks, and headings that
   separate sections rather than shout.
   =============================================================== */

/* Post header */
.post-head { display: flex; flex-direction: column; gap: var(--s-sm); }
.post-head h1 { max-width: 22ch; }

.post-meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2xs) var(--s-md);
  font-family: var(--font-mono);
  font-size: var(--t-sm);
  color: var(--c-faint);
}
.post-meta__item { display: inline-flex; align-items: center; gap: var(--s-3xs); }
.post-meta__item::before {
  content: "";
  width: 5px; height: 5px;
  border-radius: var(--r-full);
  background: var(--c-accent);
  flex-shrink: 0;
}

/* Article body — 68ch is the readable measure for this size.

   `body` prefix, per the cascade note at the top of this file. The blog
   post template puts .post-body on the same element as Oxygen's
   .bde-themeless-template-content-area, which carries max-width:100% and
   is emitted after this stylesheet. At equal specificity Oxygen won on
   source order, so the measure was silently dropped and article text ran
   the full container — 104 characters per line at 18px, against the
   60-75 that is comfortable to read.

   This was masked while .container--narrow capped its own width at 720px;
   moving that cap onto the container's children exposed it, because the
   Oxygen class beats that rule at equal specificity too. One element in
   the selector (0,0,1,1 vs 0,0,1,0) settles it without !important.

   The figure breakout below is measured against this column, so it only
   centres correctly once this is restored. */
body .post-body { max-width: 68ch; }

.post-body > * + * { margin-top: var(--s-md); }

.post-body p { font-size: var(--t-base); line-height: 1.75; }

.post-body h2 {
  font-size: var(--t-2xl);
  margin-top: var(--s-xl);
  padding-top: var(--s-md);
  border-top: 1px solid var(--c-border);
}
.post-body h3 { font-size: var(--t-lg); margin-top: var(--s-lg); }

.post-body ul, .post-body ol { padding-left: 1.3rem; display: flex; flex-direction: column; gap: var(--s-2xs); }
.post-body li { line-height: 1.7; }
.post-body li::marker { color: var(--c-accent); }

.post-body a { text-decoration: underline; }

.post-body strong { color: var(--c-text); }

.post-body blockquote {
  margin: var(--s-lg) 0;
  padding: var(--s-sm) var(--s-md);
  border-left: 3px solid var(--c-accent);
  background: var(--c-surface);
  border-radius: 0 var(--r-md) var(--r-md) 0;
}
.post-body blockquote p { font-size: var(--t-lg); color: var(--c-text); margin: 0; }

.post-body code {
  font-family: var(--font-mono);
  font-size: 0.88em;
  background: var(--c-raised);
  color: var(--c-accent-2);
  padding: 0.12em 0.35em;
  border-radius: var(--r-sm);
  border: 1px solid var(--c-border);
}

.post-body pre {
  background: #030504;
  border: 1px solid var(--c-border);
  border-radius: var(--r-md);
  padding: var(--s-sm);
  overflow-x: auto;          /* long lines scroll, the page does not */
  font-size: 0.85rem;
  line-height: 1.6;
}
.post-body pre code { background: none; border: 0; padding: 0; color: var(--c-accent-2); }

.post-body img {
  max-width: 100%;
  height: auto;
  border-radius: var(--r-md);
  border: 1px solid var(--c-border);
}

.post-body hr { border: 0; border-top: 1px solid var(--c-border); margin: var(--s-xl) 0; }

.post-body table { width: 100%; border-collapse: collapse; font-size: var(--t-sm); }
.post-body th, .post-body td { text-align: left; padding: var(--s-2xs) var(--s-xs); border-bottom: 1px solid var(--c-border); }
.post-body th { color: var(--c-text); font-family: var(--font-mono); font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.06em; background: var(--c-surface); }

/* Blog index cards */
.post-card { display: flex; flex-direction: column; gap: var(--s-2xs); }
.post-card h2 { font-size: var(--t-xl); }
.post-card p { font-size: var(--t-sm); max-width: 46ch; }
.post-card__more { margin-top: auto; padding-top: var(--s-sm); }

/* The title link inherited the global accent colour and the browser's
   default underline, so both headlines rendered as solid green underlined
   blocks — the heaviest thing on the page, and hard to read at headline
   size. A card title is a heading that happens to be clickable: it reads
   as a heading, and shows it is a link on hover, where the whole card is
   already a target anyway. */
.post-card h2 a,
.post-card a:has(h2) {
  color: var(--c-text);
  text-decoration: none;
}
.post-card h2 a:hover,
.post-card a:has(h2):hover { color: var(--c-accent); }

/* The date/read-time row sat directly on the card's top rule with no
   air, which made the rule read as an underline for the meta rather
   than as the card's edge. */
.post-card .post-meta { margin-bottom: var(--s-2xs); }

/* The index intro and the posts it introduces are one thought, but they
   are two sections, so the full section rhythm applied between them and
   left roughly 210px of empty black in the middle of a short page. The
   list keeps the rule that separates it; it just stops being pushed a
   screen away from the sentence that sets it up. */
.section:has(.post-card) { padding-top: var(--s-2xl); }

/* ---------------------------------------------------------------
   Services catalogue. The services page carries eight offerings,
   which is too long to scan without a way in, so the top of the
   page indexes them as chips that jump to each section.
   --------------------------------------------------------------- */

/* Jump target. Empty and zero-height, sitting immediately before the
   section it names — scroll-margin keeps the sticky header from
   covering the heading when the anchor is followed. */
.anchor {
  display: block;
  height: 0;
  scroll-margin-top: calc(var(--nav-h, 72px) + var(--s-md));
}

/* .badge as a link. The base badge is a neutral outlined chip, so it
   only needs a hover state to read as interactive. */
a.badge {
  text-decoration: none;
  color: var(--c-muted);
  transition: color 120ms ease, border-color 120ms ease;
}
a.badge:hover,
a.badge:focus-visible { color: var(--c-accent); }

/* Numbered eyebrow on each service, so the page reads as a catalogue
   rather than eight unrelated sections. */
.svc-n { color: var(--c-accent); }

/* Figures in posts. A diagram is usually wider than the 68ch measure the
   prose is set to, so it breaks out of the column rather than being
   shrunk to fit text width — and captions itself underneath. */
.post-body figure {
  margin: var(--s-lg) 0;
  width: 100%;
}

/* A diagram needs more width than a 68ch reading measure allows: at column
   width the labels scale down to roughly 7px and stop being readable, so
   once there is room the figure breaks out of the column.

   It breaks out rightwards from the column's left edge rather than
   centring on the column. The old `margin-left:50%; translateX(-50%)`
   assumed .container--narrow centred its column in the viewport, so the
   column's centre and the viewport's centre were the same point. Now the
   column is left-aligned with the rest of the page, that pair centred a
   1000px figure on a 780px column and pushed 70px of it off the left of
   the screen. Anchoring left also puts the figure on the same spine as
   every other element, and it cannot overflow. */
@media (min-width: 60rem) {
  .post-body figure {
    width: min(1000px, calc(100vw - var(--gutter) * 2));
  }
}
.post-body figure img,
.post-body figure svg {
  width: 100%;
  height: auto;
  display: block;
  border: 1px solid var(--c-border);
  border-radius: var(--r-md);
  background: var(--c-bg);
}
/* Below the breakout width the diagram would render its labels at around
   3px, which is no diagram at all. The image gets its own scroll container
   at a legible minimum width — the same behaviour the code blocks already
   have — while the caption stays put. The alt text carries the full content
   for anyone who would rather not swipe. */
@media (max-width: 59.99rem) {
  .post-body .fig-scroll {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    border-radius: var(--r-md);
  }
  .post-body .fig-scroll img { min-width: 860px; }
}

/* Caption in the body face, not mono. A caption under a broken-out
   figure is as wide as the figure — up to 1000px — and a full sentence
   of monospace at that length is genuinely hard to read. Capped to a
   reading measure of its own so it does not run the diagram's width. */
.post-body figcaption {
  margin-top: var(--s-xs);
  font-family: var(--font-sans);
  font-size: var(--t-sm);
  line-height: 1.6;
  color: var(--c-faint);
  max-width: 68ch;
}

/* ---------------------------------------------------------------
   Document sections.

   A privacy policy is one continuous document, but it is built as
   eight separate .section blocks — one per heading. .section carries
   --section-y (up to 7.5rem) of padding top and bottom, sized for
   major divisions of a landing page. Stacked eight deep that put
   roughly 390px of blank space between one paragraph and the next
   heading, which reads as broken rather than generous.

   Applied only to pages that are prose under headings. Landing pages
   keep the full section rhythm.
   --------------------------------------------------------------- */
.section--doc { padding-block: var(--s-md); }

/* Inside a document section the stack gap owns the vertical rhythm,
   so the blocks must not add their own margins on top of it —
   otherwise a 24px gap plus an 18px margin either side renders as
   60px between every paragraph. */
.section--doc .oxy-rich-text > :first-child { margin-top: 0; }
.section--doc .oxy-rich-text > :last-child  { margin-bottom: 0; }

/* Headings still need more air above them than sits between two
   paragraphs, or the document loses its structure. */
.section--doc h2 { margin-top: var(--s-md); }

/* ---------------------------------------------------------------
   Header logo.

   Sized in CSS rather than by the image file, so the asset can be
   swapped for a larger or smaller one without changing the layout.
   The file is 560px wide — 2x — and displays at 260px on desktop,
   scaling down on narrow screens so it never crowds the hamburger.

   aspect-ratio reserves the space before the image decodes, which
   stops the header jumping on first paint.
   --------------------------------------------------------------- */
/* The logo is a <picture>: logo-jameskdesigns-header.svg (tagline re-set
   at 1.9x) above 760px, logo-jameskdesigns-wordmark.svg (no tagline) on
   phones, where even the larger tagline is under 5px. The Oxygen HtmlCode
   wrapper and the link both need to shrink with the flex bar, hence the
   min-width:0 on each layer. */
.nav__brandwrap { min-width: 0; display: flex; }
.nav__brand { display: block; min-width: 0; line-height: 0; }
.nav__brand:focus-visible { outline: 2px solid var(--c-accent); outline-offset: 4px; border-radius: 2px; }
.nav__logo {
  display: block;
  min-width: 0;
  width: clamp(150px, 42vw, 260px);
  max-width: 100%;
  height: auto;
  aspect-ratio: 1924 / 394;
}
@media (max-width: 760px) {
  /* Wordmark-only file: shorter canvas, and the width it saves goes
     back into the wordmark so it is legible on a phone. */
  .nav__logo { aspect-ratio: 1924 / 352; width: min(54vw, 220px); }
}

/* The bar itself must never be the thing that overflows. */
.nav__inner { min-width: 0; flex-wrap: nowrap; }
.nav__inner > * { min-width: 0; }

/* Hero mark. Vector, so it is sized by the layout rather than by the
   file, and stays crisp at any width. */
/* Full lockup in the hero, as opposed to .hero__mark below which sizes
   the JK mark on its own. The lockup is 4.88:1, so it wants most of the
   column's width to carry any weight at all — at the mark's 460px it
   would be a 94px band. Kept as its own class so swapping the hero
   between the two is a one-line change in the build script. */
.hero__logo {
  display: block;
  width: 100%;
  max-width: 560px;
  height: auto;
  aspect-ratio: 1924 / 394;
  margin-inline: auto;
}

/* Portrait on the About page.
   The photo is a circular headshot with its own glow, but the file is a
   square JPEG whose corners are #14222F — a dark blue that reads as a
   visible box against the near-black green ground. The circular mask
   removes them. aspect-ratio guards it: a 50% radius on a non-square
   image gives an ellipse, and the source is only square because it was
   cropped to be. Held at 150px because the original is 188px wide, so
   anything larger is upscaling a thumbnail. */
.about-portrait {
  display: block;
  width: clamp(120px, 28vw, 150px);
  /* height:auto so the width/height attributes on the img (there for CLS)
     do not override the aspect-ratio below. */
  height: auto;
  aspect-ratio: 1;
  object-fit: cover;
  border-radius: var(--r-full);
  margin-bottom: var(--s-2xs);
}

.hero__mark {
  display: block;
  width: 100%;
  /* Was 520 when the mark shipped with a bloom around it. Most of that
     width was empty canvas. The viewBox is cropped to the art now, so
     the same visual size needs a smaller box. */
  max-width: 460px;
  height: auto;
  margin-inline: auto;
}

/* ---------------------------------------------------------------
   404 floor.

   The 404 carries one short block of content, so the document ended
   around 520px and the footer landed halfway up the screen with empty
   ground beneath it. That is what made the page feel unfinished; the
   header was never missing, it just looks absent in the Oxygen builder
   because a template previews its own content without the header and
   footer wrappers.

   A floor on the hero pushes the footer to the bottom of the viewport
   without the sitewide flex-column change a true sticky footer needs.
   175px is the header and footer measured together. Scoped to
   body.error404 so no other page is touched.
   --------------------------------------------------------------- */
body.error404 .hero {
  min-height: calc(100vh - 175px);
  display: flex;
  align-items: center;
}

/* ---------------------------------------------------------------
   About hero. Heading on the left, portrait on the right.
   Two columns rather than the stacked version it replaced: the photo
   was sitting above the H1 and pushing it down the page, and a face
   beside the name reads as a person rather than as decoration.
   `auto` on the second track means the column is exactly the portrait's
   width, so the heading keeps everything else.
   --------------------------------------------------------------- */
.about-hero {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: var(--s-xl);
  align-items: center;
}

/* Below the hero's own breakpoint there is not enough room for two
   columns without squeezing the heading to three or four words a line,
   so it collapses — and the portrait goes back above the heading, which
   is where it was and where it belongs on a phone. */
@media (max-width: 760px) {
  .about-hero { grid-template-columns: 1fr; gap: var(--s-md); justify-items: start; }
  .about-hero .about-portrait { order: -1; }
}

/* Bigger here than in the old stacked position: it has a column of its
   own to fill and sits next to display-size type. Still capped at the
   source file's 188px, because anything above that is upscaling. */
@media (min-width: 761px) {
  .about-hero .about-portrait {
    width: clamp(140px, 16vw, 188px);
    margin-bottom: 0;
  }
}
