/* ════════════════════════════════════════════════════
   DESIGN TOKENS
   Every clamp pairs rem + vw so a user's browser font-size
   setting still scales the page. Values interpolate between
   a 320px and a 1440px viewport.
   Colour tokens live in color-schemes.css.
   ════════════════════════════════════════════════════ */
:root {
  /* ── Fluid type ─────────────────────────────────── */
  --step--1: clamp(0.68rem, 0.65rem + 0.14vw, 0.80rem);
  --step-0:  clamp(0.95rem, 0.90rem + 0.25vw, 1.125rem);
  --step-1:  clamp(1.05rem, 0.96rem + 0.43vw, 1.35rem);
  --step-2:  clamp(1.15rem, 0.98rem + 0.86vw, 1.75rem);
  --step-3:  clamp(1.60rem, 1.39rem + 1.07vw, 2.35rem);
  --step-4:  clamp(2.40rem, 1.83rem + 2.86vw, 4.40rem);
  /* Solutions headings only. Separate from step-4 because that one is shared
     with the footer wordmark, which should not grow with this section. */
  --step-5:  clamp(3.20rem, 2.40rem + 4.00vw, 6.00rem);

  /* ── Fluid space ────────────────────────────────── */
  --space-xs:  clamp(0.375rem, 0.30rem + 0.36vw, 0.625rem);
  --space-sm:  clamp(0.625rem, 0.52rem + 0.54vw, 1rem);
  --space-md:  clamp(1rem,     0.86rem + 0.71vw, 1.5rem);
  --space-lg:  clamp(1.5rem,   1.21rem + 1.43vw, 2.5rem);
  --space-xl:  clamp(2.25rem,  1.89rem + 1.79vw, 3.5rem);
  --space-2xl: clamp(3rem,     2.50rem + 2.50vw, 4.75rem);

  /* ── Layout ─────────────────────────────────────── */
  --container: 1280px;
  --measure: 68ch;
  --nav-h: 2.75rem;        /* navbar height; .hero offsets by the same */
}

/* ── Reset ─────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

body {
  font-family: 'Times New Roman', Times, serif;
  color: var(--text);
  background: var(--bg);
  /* 100vh ignores the iOS Safari toolbar, so the last ~80px sits under it.
     dvh tracks the visible viewport; the vh line stays as the fallback. */
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  -webkit-text-size-adjust: 100%;
}

/* Media never overflows its container. Rules that need a fixed height
   (.wave-panel svg, .founded-right img, the hero video's inline style)
   are more specific and still win. */
img, video, svg {
  max-width: 100%;
  height: auto;
  display: block;
}

footer { margin-top: auto; }

/* ── Container ──────────────────────────────────────
   Sections stay full-bleed so the cream/navy bands run edge
   to edge; the inline padding absorbs excess width and centres
   content at --container. Below that width this resolves to
   --space-lg and behaves like ordinary padding. */
.two-col,
.founded-section,
.philosophy-section,
.solutions-section,
.bio-section {
  padding-inline: max(var(--space-lg), (100% - var(--container)) / 2);
}

/* ── Navbar ─────────────────────────────────────── */
nav {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 1000;
  background-color: var(--nav-bg);
  height: var(--nav-h);
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding-inline: var(--space-md);
  transition: background-color 0.3s;
}

.nav-brand {
  color: #fff;
  font-family: 'Times New Roman', Times, serif;
  font-size: var(--step-1);
  font-weight: normal;
  text-decoration: none;
}

.nav-links {
  display: flex;
  align-items: center;
  list-style: none;
  gap: var(--space-md);
}

.nav-links > li > a,
.nav-links > li > span {
  color: #fff;
  text-decoration: none;
  font-family: 'Times New Roman', Times, serif;
  font-size: var(--step--1);
  cursor: pointer;
}

.nav-links > li > a:hover,
.nav-links > li > span:hover { opacity: 0.7; }

/* ── Hero ───────────────────────────────────────── */
.hero {
  margin-top: var(--nav-h);
  width: 100%;
  height: clamp(12.5rem, 6rem + 20vw, 27.5rem);
  background-size: cover;
  background-position: center 10%;
  position: relative;
}

.hero-caption {
  position: absolute;
  top: var(--space-sm);
  left: var(--space-md);
  color: #fff;
  font-family: 'Times New Roman', Times, serif;
  font-size: var(--step-1);
  line-height: 1.55;
}

/* ── Two-column section base ────────────────────── */
.two-col {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  padding-block: var(--space-lg);
  gap: var(--space-sm) 0;
  transition: background-color 0.3s;
}

.two-col.cream { background-color: var(--bg-secondary); }
.two-col.white { background-color: var(--bg); }

.col-label {
  flex: 0 0 22%;
  font-family: 'Times New Roman', Times, serif;
  font-size: var(--step-1);
  color: var(--text-muted);
  padding-top: var(--space-xs);
  line-height: 1.4;
}

.col-content { flex: 1 1 20rem; }

/* Large serif list (We are / We build on) */
.large-list p {
  font-family: 'Times New Roman', Times, serif;
  font-size: var(--step-3);
  line-height: 1.45;
  color: var(--text);
}

/* ── Founded by background ticker ───────────────── */
.fba-wrap {
  position: absolute;
  inset: 0 0 0 66.67%;
  overflow: hidden;
  pointer-events: none;
  z-index: 0;
  display: flex;
  flex-direction: column;
  justify-content: space-around;
  opacity: 0.07;
}
.fba-row {
  overflow: hidden;
  white-space: nowrap;
  font-family: 'Times New Roman', Times, serif;
  font-size: var(--step-3);
  line-height: 1;
  color: var(--text);
  -webkit-user-select: none;
  user-select: none;
}
.fba-track {
  display: inline-block;
  white-space: nowrap;
}
.fba-row--rtl .fba-track { animation: fbaRTL 18s linear infinite; }
.fba-row--ltr .fba-track { animation: fbaLTR 18s linear infinite; }

@keyframes fbaRTL {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}
@keyframes fbaLTR {
  from { transform: translateX(-50%); }
  to   { transform: translateX(0); }
}

/* ── Founded by ─────────────────────────────────── */
.founded-section {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  padding-block: var(--space-lg) var(--space-2xl);
  background: var(--bg);
  gap: var(--space-md) 0;
  transition: background-color 0.3s;
  position: relative;
}
.founded-left,
.founded-right { position: relative; z-index: 1; }

.founded-left { flex: 1 1 22rem; }

.founded-left .col-label { flex: initial; margin-bottom: var(--space-sm); }

.founded-names p {
  font-family: 'Times New Roman', Times, serif;
  font-size: var(--step-2);
  color: var(--text-secondary);
  line-height: 1.6;
}

/* Portraits wrap and scale rather than sitting at a fixed 238px height,
   which used to push three images past the viewport edge on a phone. */
.founded-right {
  flex: 1 1 18rem;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
  align-items: flex-start;
  justify-content: flex-end;
  padding-top: var(--space-xs);
}

/* Each portrait links to that person's bio on the About page. line-height:0
   kills the baseline gap an inline anchor would otherwise leave under the
   image. */
.founded-right a {
  display: block;
  line-height: 0;
}

.founded-right img {
  height: clamp(7rem, 3rem + 12vw, 14.875rem);
  width: auto;
  filter: grayscale(100%);
  transition: filter 0.3s;
}

/* Colour returning on hover is the affordance — without it there is nothing
   to signal these became clickable. Stilled by the reduced-motion block. */
.founded-right a:hover img,
.founded-right a:focus-visible img {
  filter: grayscale(0%);
}

/* ── Solutions ──────────────────────────────────── */
.solutions-section {
  /* Wider than the 1280px used elsewhere. With a 360px cube and a heading
     column big enough for "Monitor" at 6rem, 1280px leaves too little for the
     description to wrap down to the cube's height. Feeds the shared
     padding-inline rule above. */
  --container: 1600px;
  padding-block: var(--space-lg);
  background: var(--bg-secondary);
  transition: background-color 0.3s;
}

.solutions-section > .col-label { flex: initial; margin-bottom: var(--space-md); }

.solutions-cols {
  display: flex;
  flex-direction: column;
  /* A container query never styles its own container element, and
     .sol-col / .sol-col-with-wave are the same element. Without a container
     one level up, the mobile `.sol-col-with-wave` rule below is dead and the
     desktop 3-column grid survives onto phones. Same content width as
     .sol-col (which only has padding-block), so every existing @container
     threshold keeps its current meaning. */
  container-type: inline-size;
}

.sol-col {
  /* Was padding-bottom only. Padding on both sides roughly doubles each
     column's contribution to the section height. */
  padding-block: var(--space-2xl);
  /* Lets the description key its justification off this column's own width
     rather than the viewport's — see the @container rule below. */
  container-type: inline-size;
}

/* Text | wave panel | description. The description gets the wider track,
   matching the original calc(40vw) / 240px / calc(50vw) proportions.
   Collapsing is handled by the @container rule further down. */
.sol-col-with-wave {
  display: grid;
  /* 1 : 2 at full width — heading ~410px, description ~830px, which wraps
     632 characters to about 8 lines ≈ the 360px cube.

     min-content (not 0) on the heading track is load-bearing: with a 0 floor
     the track shrinks below the width "Monitor" needs at its clamped size and
     the heading spills over the cube on any window between ~1024 and 1600px.
     The description absorbs the shortfall instead. */
  grid-template-columns: minmax(min-content, 1fr) auto minmax(0, 2fr);
  /* Tops of the heading and the description line up; the cube stays centred
     against the row via align-self below. */
  align-items: start;
}

.sol-col-text {
  min-width: 0;
  /* stretch, not end: `end` shrink-wraps each block to its own text and pins
     it right, so "AVMs" / "Monitor" / "Risk" each start at a different left
     edge. Filling the track lines all three up. */
  justify-self: stretch;
  padding-left: 5%;
  padding-right: clamp(1.5rem, 10%, 5rem);
}

.wave-panel {
  /* MUST be a definite width AND height. The canvas script reads
     offsetWidth/offsetHeight inline during parse and returns permanently
     if either is 0 — it never retries. A percentage width or an
     aspect-ratio-derived height is not resolvable at that moment, which
     silently kills the animation. Keep both sides explicit. */
  /* +50%: 15rem → 22.5rem at full size. The canvas scripts read
     offsetWidth/offsetHeight, so they re-render at the new size on their own. */
  width:  clamp(16.5rem, 13.5rem + 9vw, 22.5rem);
  height: clamp(16.5rem, 13.5rem + 9vw, 22.5rem);
  justify-self: center;
  align-self: center;
  position: relative;
  overflow: hidden;
  background: transparent;
}

.sol-reveal-char {
  opacity: 0;
  display: inline;
}

.sol-cursor {
  display: none;
  position: fixed;
  pointer-events: none;
  z-index: 9999;
  font-family: 'Times New Roman', Times, serif;
  font-size: var(--step-0);
  color: var(--text-secondary);
  line-height: 1;
  animation: solCursorBlink 0.7s step-end infinite;
}

@keyframes solCursorBlink {
  0%, 100% { opacity: 1; }
  50% { opacity: 0; }
}

.sol-col-desc {
  min-width: 0;
  padding-left: clamp(1.5rem, 10%, 5rem);
  font-family: 'Times New Roman', Times, serif;
  font-size: var(--step-1);
  line-height: 2.1;
  color: var(--text-secondary);
  text-align: left;
}

/* Marker-pen highlight on the closing call-to-action sentence of each
   Solutions paragraph. --accent is a dark navy, so the text inverts to
   stay readable.

   Softened by mixing toward the cream rather than by going translucent:
   adjacent per-character spans with a see-through background can show
   darker seams where their boxes meet. This stays fully opaque. */
.sol-highlight {
  --highlight: color-mix(in srgb, var(--accent) 85%, var(--bg-secondary));

  /* Fallback only — applies if the reveal script never runs, so the text is
     never white-on-cream and invisible. :has() switches it off below. */
  background-color: var(--highlight);
  color: var(--bg);
  padding: 0.08em 0.28em;
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone;
}

/* Once the script has split the text into characters, hand the background
   over to them. Each character span is opacity:0 until revealed, and opacity
   hides the background too — so the highlight now paints in with the text
   instead of sitting there as a solid bar beforehand. */
.sol-highlight:has(.sol-reveal-char) {
  background-color: transparent;
  padding: 0;
}

.sol-highlight .sol-reveal-char {
  background-color: var(--highlight);
  padding-block: 0.08em;
  /* Character advances are fractional, so each background box rounds to the
     pixel grid on its own and neighbours leave hairline gaps. Pad outward and
     pull back by the same amount: the boxes overlap instead of just touching,
     and net width is unchanged so nothing shifts. Only safe because
     --highlight is opaque — a translucent colour would darken at each seam. */
  padding-inline: 0.03em;
  margin-inline: -0.03em;
}
.sol-highlight .sol-reveal-char:first-child {
  padding-left: 0.3em;
  margin-left: 0;
}
.sol-highlight .sol-reveal-char:last-child {
  padding-right: 0.3em;
  margin-right: 0;
}

/* Justified text needs width or it opens rivers of white space, so this
   keys off the column's own width, not the viewport. */
@container (min-width: 42rem) {
  .sol-col-desc { text-align: justify; }
}

/* Below this the three tracks can't hold the headings, so stack them.
   Keyed to the column, not the viewport — the same collapse is correct
   whether the column is narrow because the screen is, or because the
   layout around it changed. */
 @container (max-width: 52rem) {
    /* Heading + keywords on the left, animation beside them and the same
       height as that block; long text on its own full-width row underneath.
       Only reachable because .solutions-cols is now a container — this
       element IS .sol-col, so it cannot match its own container query. */
    .sol-col-with-wave {
      grid-template-columns: minmax(0, 1fr) auto;
      align-items: stretch;
      /* Row gap stays at md so the description keeps its distance from the
         keywords; the column gap drops to sm to buy back width for the
         keyword track now that the panel is wider. */
      gap: var(--space-md) var(--space-sm);
    }

    .sol-col-text {
      grid-column: 1;
      justify-self: stretch;
      padding-left: 0;      /* flush with the description below it */
      padding-right: 0;
    }

    .wave-panel {
      grid-column: 2;
      justify-self: end;
      /* Spans the full height of the text block — top of "AVMs" to the bottom
         of the last keyword. height:auto is required: align-self:stretch only
         takes effect when height is auto, and the desktop rule pins a fixed
         16.5rem. Safe for the canvas because its resize() runs inside
         requestAnimationFrame (index.html:305 and :514), i.e. after layout. */
      align-self: stretch;
      height: auto;
      /* cqw, not vw: a fixed share of the column, so the text track beside it
         stays predictable and the keyword sizes below can be tuned against it.
         29cqw is the earlier 22cqw plus a third, closing the white space on
         the panel's left edge. Top, bottom and right are unaffected — those
         come from align-self: stretch and justify-self: end. */
      width: clamp(4.5rem, 29cqw, 10.5rem);
    }

    .sol-col-desc {
      grid-column: 1 / -1;
      padding-left: 0;
      /* The @container (min-width: 42rem) justify rule cannot reach a phone —
         the column is ~19rem there. Justify here too, with hyphenation, since
         a narrow measure opens rivers otherwise. */
      text-align: justify;
      -webkit-hyphens: auto;
      hyphens: auto;
    }

    /* Two class selectors on purpose: `.sol-col h3` and `.sol-col li` appear
       LATER in this file at equal specificity, and a container query adds
       none of its own, so a single-class rule here would simply lose.

       min() with no floor: the sizes must always fit the track, and a floor
       would let them overflow into the animation on very narrow screens.
       Sized for Noto Serif, which is what Android substitutes for Times and
       runs ~10% wider — the binding case is "Portfolio Optimiser" at ~8.6em. */
    .sol-col .sol-col-text h3 { font-size: min(3.2rem, 16cqw); }

    .sol-col .sol-col-text li {
      font-size: min(1.5rem, 7.4cqw);
      line-height: 1.7;
    }
  }

.wave-panel svg {
  position: absolute;
  left: -2.5%;
  top: 0;
  width: 105%;
  height: 100%;
  display: block;
}

/* Monitor waves: the generator leaves an artefact at the right end of the
   path. Trim the last 2% of the SVG. Safe because the SVG is 105% wide at
   left:-2.5%, so it spans -2.5%..102.5% of the panel — removing 2% of 105%
   (2.1 points) leaves the right edge at 100.4%, still covering the panel.
   Going much beyond 2% here would expose a gap. */
#waves-indices {
  clip-path: inset(0 2% 0 0);
}

.wave-panel canvas {
  width: 100%;
  height: 100%;
  display: block;
  background: transparent;
}

.wave-panel .wave-path {
  fill: var(--accent);
  fill-opacity: 0.15;
  stroke: var(--accent);
  stroke-opacity: 0.35;
  stroke-width: 2;
}

.sol-col h3 {
  font-family: 'Times New Roman', Times, serif;
  font-size: var(--step-5);
  /* Default (~1.15) adds half-leading above the glyph, pushing the heading
     down relative to the description's first line. 1 removes it so the two
     tops sit level. */
  line-height: 1;
  font-weight: normal;
  margin-bottom: var(--space-xs);
  color: var(--text);
  text-wrap: balance;
}

.sol-col ul { list-style: none; }

.sol-col li {
  font-family: 'Times New Roman', Times, serif;
  /* 10% under step-3. Expressed as a calc on the token rather than a hardcoded
     clamp so it keeps tracking the scale if step-3 ever changes. */
  font-size: calc(var(--step-3) * 0.9);
  color: var(--text-secondary);
  line-height: 1.7;
}

/* ── Decorative image strip ─────────────────────── */
.img-strip {
  width: 100%;
  height: clamp(5rem, 3rem + 6vw, 8.125rem);
  background-size: cover;
  background-position: center;
  display: block;
}

/* ── Footer ─────────────────────────────────────── */
footer {
  background-color: var(--footer-bg);
  color: rgba(255,255,255,0.65);
  padding: var(--space-xl) var(--space-md);
  min-height: 15rem;
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-family: 'Times New Roman', Times, serif;
  font-size: var(--step--1);
  transition: background-color 0.3s;
  position: relative;
  overflow: hidden;
}

footer a { color: rgba(255,255,255,0.65); text-decoration: none; }
footer a:hover { color: #fff; }

.footer-right {
  position: absolute;
  bottom: var(--space-md);
  right: var(--space-md);
  text-align: right;
  line-height: 1.5;
}

/* ── Footer logo ────────────────────────────────── */
.fla {
  position: absolute;
  left: var(--space-md);
  top: var(--space-md);
  font-family: 'Times New Roman', Times, serif;
  font-size: var(--step-4);
  line-height: 1;
  color: rgba(255,255,255,0.65);
  -webkit-user-select: none;
  user-select: none;
  pointer-events: none;
}

/* ── About: Bio blocks ──────────────────────────── */
.bio-section {
  background: var(--bg);
  transition: background-color 0.3s;
}

.bio-block {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: var(--space-md) 0;
  padding-block: var(--space-lg);
  /* Anchor target for the portrait links on the home page. The navbar is
     position:fixed, so without this the block scrolls flush to the top and
     the navbar sits over its first lines. */
  scroll-margin-top: calc(var(--nav-h) + var(--space-md));
}

.bio-portrait { flex: 0 0 22%; }

.bio-portrait img {
  width: 100%;
  max-width: 9.375rem;
  filter: grayscale(100%);
}

.bio-text { flex: 1 1 20rem; min-width: 0; }

.bio-text h2 {
  font-family: 'Times New Roman', Times, serif;
  font-size: var(--step-3);
  font-weight: normal;
  color: var(--text);
  margin-bottom: var(--space-xs);
  padding-bottom: var(--space-xs);
  border-bottom: 1px solid var(--border);
  display: block;
  max-width: min(100%, 46rem);
  text-wrap: balance;
}

.bio-text h3 {
  font-family: 'Times New Roman', Times, serif;
  font-size: var(--step-0);
  font-weight: normal;
  color: var(--text-muted);
  margin-bottom: var(--space-sm);
}

.bio-text ul { list-style: none; margin-top: var(--space-sm); }

.bio-text li {
  font-family: 'Times New Roman', Times, serif;
  font-size: var(--step-0);
  color: var(--text-secondary);
  line-height: 1.7;
}

/* ── About: Philosophy ──────────────────────────── */
.philosophy-section {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  padding-block: var(--space-lg);
  background: var(--bg-secondary);
  gap: var(--space-sm) 0;
  transition: background-color 0.3s;
}

.philosophy-section .col-label { flex: 0 0 22%; }

.philosophy-section blockquote {
  flex: 1 1 20rem;
  font-family: 'Times New Roman', Times, serif;
  font-style: italic;
  font-size: var(--step-1);
  color: var(--text-secondary);
  line-height: 1.65;
  max-width: var(--measure);
}

/* ── Hamburger button (hidden on desktop) ───────── */
.nav-toggle {
  display: none;
  background: none;
  border: none;
  color: #fff;
  font-size: var(--step-1);
  cursor: pointer;
  padding-inline: var(--space-xs);
  line-height: 1;
}

/* ── Sofie's portrait: square source, centred crop ──
   The source is 800x800 while the other two are tall portraits, so it
   rendered much wider than them. Pinning the same 0.715 ratio and letting
   object-fit: cover crop from the centre takes an equal sliver off each
   side — no wrapper needed, unlike Maarten's off-centre re-frame below. */
.portrait-fit {
  aspect-ratio: 0.715;
  object-fit: cover;
}

/* ── Maarten's portrait: re-framed crop ─────────────
   Source is 1200x1600 and the subject sits low and right, so drop the left
   20% and top 25% and rescale the remainder to fill the original footprint
   (ratio 0.715, same as the image it replaced).

   Geometry, all relative to the wrapper so it holds at any size:
     scale so the lower 75% of the source fills the box -> height 133.33%
     1200px of source at that scale                     -> width  139.86%
     shift up by 25% of source height                   -> top   -33.33%
     shift left by 20% of source width                  -> left  -27.97%
   Width x height reproduces the source's 0.750 ratio exactly, so no stretch.

   Must come after .founded-right img and .bio-portrait img: same specificity
   (0,1,1), so source order decides. Only the box-geometry properties are
   overridden — greyscale and the hover transition still come from those
   rules. */
.portrait-crop {
  display: block;
  position: relative;
  overflow: hidden;
  aspect-ratio: 0.715;
}

.founded-right .portrait-crop { height: clamp(7rem, 3rem + 12vw, 14.875rem); }
.bio-portrait  .portrait-crop { width: 100%; max-width: 9.375rem; }

.portrait-crop img {
  position: absolute;
  top: -33.33%;
  left: -27.97%;
  width: 139.86%;
  height: 133.33%;
  max-width: none;      /* the global img rule caps width at 100% */
  object-fit: cover;    /* guards against rounding drift in the ratio */
}

/* ── Inline text links ──────────────────────────────
   Accent blue plus bold, no underline. Bold carries the weight the underline
   would have: it keeps the link distinguishable from the surrounding grey by
   something other than hue alone, which matters for anyone who cannot tell
   the two colours apart. Matches the treatment used on the Solutions
   call-to-action sentences. */
.inline-link {
  color: var(--accent);
  font-weight: 700;
  text-decoration: none;
  transition: opacity 0.2s;
}

.inline-link:hover {
  opacity: 0.7;
}

/* ── Keyboard focus ─────────────────────────────── */
/* :focus-visible only shows for keyboard users, so mouse clicks stay clean. */
a:focus-visible,
button:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}
nav a:focus-visible,
.nav-toggle:focus-visible {
  outline: 2px solid #fff;
  outline-offset: 3px;
}

/* ── Reduced motion ─────────────────────────────── */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
  /* Reveal text is opacity:0 until the scroll script paints it — make sure it
     is simply visible rather than never appearing. */
  .sol-reveal-char { opacity: 1 !important; }
  .sol-cursor { display: none !important; }
}

/* ── Structural: nav collapse and column stacking ──
   The only media query left. Everything above sizes itself fluidly;
   these are structural changes (horizontal bar → drawer, side-by-side
   → stacked) which a breakpoint expresses more honestly than a clamp. */
@media (max-width: 768px) {

  .nav-toggle { display: block; }

  .nav-links {
    display: none;
    flex-direction: column;
    position: absolute;
    top: var(--nav-h);
    left: 0; right: 0;
    background: var(--nav-bg);
    padding-block: var(--space-xs);
    gap: 0;
    z-index: 999;
  }
  .nav-links.open { display: flex; }

  .nav-links > li > a,
  .nav-links > li > span {
    display: block;
    padding: var(--space-sm) var(--space-md);
    font-size: var(--step-0);
  }

  /* Labels and portraits take the full row instead of a 22% column. */
  .col-label,
  .bio-portrait,
  .philosophy-section .col-label { flex: 1 1 100%; }

  /* --step-3 overshoots the available width on narrow phones, dropping
     "Pricing" off "Experienced in Asset Pricing" onto a fourth line (~7% over
     at 360px). The vw cap keeps the longest line — 12.7em in Noto Serif, the
     Android substitute for Times — on one row down to 320px. It is a no-op
     from roughly 430px up, where --step-3 is already the smaller of the two,
     so tablet and desktop sizing is unchanged. */
  .large-list p { font-size: min(var(--step-3), 6.3vw); }

  .founded-right { justify-content: flex-start; }
}
